外观
消息通知
概述
消息通知能力封装在后端框架层,为业务系统提供统一的短信与邮件发送入口。
短信 SMS
短信框架位于 app/framework/sms,采用策略模式 + 工厂模式:每种短信厂商实现一个客户端子类,屏蔽厂商 SDK 差异;工厂负责按渠道配置创建并缓存客户端,调用方只依赖统一接口。
设计核心
SmsChannelEnum:渠道编码枚举(如ALIYUN、TENCENT、HUAWEI、QINIU、DINGTALK),是业务层与框架层共用的单一真源,新增渠道必须在此登记,避免两处枚举值漂移。SmsChannelProperties:渠道配置属性(Pydantic 模型),用于初始化具体客户端。extra="ignore"使其可宽松接收数据库行字段。关键映射:api_key→ 厂商 AccessKeyId / SecretIdapi_secret→ 厂商 AccessKeySecret / SecretKeysignature→ 短信签名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):按编码解析回执;即便该渠道从未发送过(未初始化),也会临时构造一个轻量解析实例(解析不需密钥),保证回调始终可用。
厂商实现示例
| 厂商 | 客户端 | 关键要求 |
|---|---|---|
| 阿里云 | AliyunSmsClient | signature 必填;模板参数为 JSON 字符串,键名对应模板变量 |
| 腾讯云 | TencentSmsClient | sms_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)其内部流程:
- 按
template_code查询短信模板,不存在则报错; - 按模板关联渠道读取渠道配置;
- 先创建一条
发送中(INIT)的短信日志(记录手机号、模板内容、渲染后的正文等); - 用渠道配置构造
SmsChannelProperties,经工厂create_or_update_sms_client拿到客户端; - 调用
client.send_sms(...),将结果(send_status、API 返回码/流水号)回写日志; - 接收状态(
receive_status)由厂商异步回调更新,发送阶段保持INIT。
模板内容支持 {name} / { code } 风格占位符,发送前通过 render_template 用 template_params 渲染。
新增一个短信渠道
- 在
SmsChannelEnum登记新编码; - 在
app/framework/sms/新建provider_xxx.py,实现AbstractSmsClient子类(至少实现send_sms、parse_sms_receive_status); - 在
factory.py的_CHANNEL_CLIENT_CLASS注册编码 → 客户端类; - 如需密钥,在
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/*
前端创建/更新模板时只需选择渠道或账号、填写内容,参数数组由后端按占位符自动提取;发送后可在日志页查看发送/接收状态与厂商返回的明细。