目录
- 消息通知系统实现:构建高可用、可扩展的企业级通知服务
-
- [1. 引言](#1. 引言)
- [2. 系统架构设计](#2. 系统架构设计)
-
- [2.1 整体架构](#2.1 整体架构)
- [2.2 核心组件](#2.2 核心组件)
- [2.3 技术栈选择](#2.3 技术栈选择)
- [3. 数据模型设计](#3. 数据模型设计)
-
- [3.1 核心实体关系](#3.1 核心实体关系)
- [4. 核心功能实现](#4. 核心功能实现)
-
- [4.1 消息模板系统](#4.1 消息模板系统)
- [4.2 渠道适配器](#4.2 渠道适配器)
- [4.3 通知服务核心](#4.3 通知服务核心)
- [4.4 API服务](#4.4 API服务)
- [5. 使用示例](#5. 使用示例)
- [6. 系统部署与监控](#6. 系统部署与监控)
-
- [6.1 部署架构](#6.1 部署架构)
- [6.2 监控指标](#6.2 监控指标)
- [6.3 告警规则](#6.3 告警规则)
- [7. 代码自查与优化](#7. 代码自查与优化)
-
- [7.1 代码质量检查清单](#7.1 代码质量检查清单)
- [7.2 优化建议](#7.2 优化建议)
- [8. 总结](#8. 总结)
-
- [8.1 系统特点](#8.1 系统特点)
- [8.2 实际应用场景](#8.2 实际应用场景)
- [8.3 后续改进方向](#8.3 后续改进方向)
『宝藏代码胶囊开张啦!』------ 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 "白菜价"+"量身定制 "!无论是卡脖子的毕设/课设/文献复现 ,需要灵光一现的算法改进 ,还是想给项目加个"外挂",这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网
消息通知系统实现:构建高可用、可扩展的企业级通知服务
1. 引言
在现代分布式系统中,消息通知系统是不可或缺的组成部分。它负责在不同组件、服务和用户之间传递重要信息,确保系统的各个部分能够及时响应状态变化。一个设计良好的通知系统不仅能提升用户体验,还能帮助系统管理员监控系统状态,及时发现和解决问题。
本文将详细探讨如何设计并实现一个完整的消息通知系统,涵盖多种通知渠道(邮件、短信、站内信、推送通知等)、消息模板管理、异步处理、失败重试、监控告警等核心功能。我们将使用Python构建一个可扩展、高可用的通知服务。
2. 系统架构设计
2.1 整体架构
支持服务
通知服务
生产者
事件
事件
告警
消费者
用户邮箱
邮件通知
用户手机
短信通知
用户设备
推送通知
用户账户
站内信通知
业务系统
API网关
定时任务
监控系统
消息接收器
消息队列
消息处理器
渠道分发器
邮件渠道
短信渠道
推送渠道
站内信渠道
发送结果
模板管理
用户偏好
发送记录
失败重试
监控告警
2.2 核心组件
- 消息接收器:接收来自各种来源的通知请求
- 消息队列:使用消息队列进行异步处理,提高系统吞吐量
- 消息处理器:解析消息内容,应用模板,准备发送数据
- 渠道分发器:根据用户偏好和消息类型选择合适的发送渠道
- 渠道适配器:封装不同通知渠道的具体实现
- 模板引擎:管理消息模板,支持变量替换
- 用户偏好管理:存储和管理用户的通知偏好设置
- 发送记录:记录所有发送操作,用于追踪和审计
- 失败重试机制:处理发送失败的情况,进行重试
- 监控告警:监控系统状态,及时发现异常
2.3 技术栈选择
- 消息队列:RabbitMQ / Redis / Kafka
- 数据库:PostgreSQL / MySQL (存储用户偏好、模板、发送记录)
- 缓存:Redis (存储模板缓存、用户偏好缓存)
- 邮件服务:SMTP / SendGrid / AWS SES
- 短信服务:Twilio / 阿里云短信 / 腾讯云短信
- 推送服务:Firebase Cloud Messaging / Apple Push Notification Service
- Web框架:FastAPI / Flask (提供API接口)
- 异步任务:Celery / asyncio
- 监控:Prometheus + Grafana
3. 数据模型设计
3.1 核心实体关系
has
receives
uses
delivers_via
User
string
id
PK
string
email
string
phone
string
name
datetime
created_at
datetime
updated_at
UserPreference
string
id
PK
string
user_id
FK
string
notification_type
string
channel
boolean
enabled
jsonb
settings
datetime
created_at
datetime
updated_at
Notification
string
id
PK
string
user_id
FK
string
template_id
FK
string
channel_id
FK
string
title
string
content
jsonb
metadata
string
status
datetime
scheduled_at
datetime
sent_at
datetime
read_at
int
retry_count
datetime
created_at
datetime
updated_at
NotificationTemplate
string
id
PK
string
name
string
type
string
content
jsonb
variables
string
channel
datetime
created_at
datetime
updated_at
NotificationChannel
string
id
PK
string
name
string
type
jsonb
config
boolean
enabled
int
priority
datetime
created_at
datetime
updated_at
4. 核心功能实现
4.1 消息模板系统
python
# templates/template_manager.py
import re
import json
import hashlib
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime
from abc import ABC, abstractmethod
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class TemplateType(Enum):
"""模板类型枚举"""
EMAIL = "email"
SMS = "sms"
PUSH = "push"
IN_APP = "in_app"
WEBHOOK = "webhook"
class TemplateEngine(ABC):
"""模板引擎抽象基类"""
@abstractmethod
def render(self, template: str, context: Dict[str, Any]) -> str:
"""渲染模板"""
pass
@abstractmethod
def validate(self, template: str) -> Tuple[bool, List[str]]:
"""验证模板语法"""
pass
@abstractmethod
def extract_variables(self, template: str) -> List[str]:
"""提取模板中的变量"""
pass
class SimpleTemplateEngine(TemplateEngine):
"""简单模板引擎,使用 {{variable}} 语法"""
def __init__(self):
# 正则表达式匹配 {{variable}} 格式
self.variable_pattern = re.compile(r'{{\s*([^{}]+)\s*}}')
def render(self, template: str, context: Dict[str, Any]) -> str:
"""渲染模板
Args:
template: 模板字符串
context: 变量上下文
Returns:
渲染后的字符串
"""
def replace_match(match):
var_name = match.group(1).strip()
# 支持嵌套属性访问,如 user.name
value = context
for key in var_name.split('.'):
if isinstance(value, dict) and key in value:
value = value[key]
elif hasattr(value, key):
value = getattr(value, key)
else:
# 变量未找到,返回原模板占位符
return match.group(0)
return str(value) if value is not None else ""
try:
return self.variable_pattern.sub(replace_match, template)
except Exception as e:
logger.error(f"模板渲染失败: {str(e)}")
raise
def validate(self, template: str) -> Tuple[bool, List[str]]:
"""验证模板语法
Args:
template: 模板字符串
Returns:
(是否有效, 错误列表)
"""
errors = []
# 检查未闭合的标签
stack = []
for i, char in enumerate(template):
if char == '{':
stack.append(i)
elif char == '}':
if not stack:
errors.append(f"多余的 '}}' 在位置 {i}")
else:
stack.pop()
if stack:
for pos in stack:
errors.append(f"未闭合的 '{{' 在位置 {pos}")
# 检查变量语法
matches = list(self.variable_pattern.finditer(template))
for match in matches:
var_name = match.group(1).strip()
if not var_name:
errors.append(f"空变量名在位置 {match.start()}")
elif not re.match(r'^[a-zA-Z_][a-zA-Z0-9_.]*$', var_name):
errors.append(f"无效的变量名 '{var_name}' 在位置 {match.start()}")
return len(errors) == 0, errors
def extract_variables(self, template: str) -> List[str]:
"""提取模板中的变量
Args:
template: 模板字符串
Returns:
变量名列表
"""
matches = self.variable_pattern.findall(template)
return [match.strip() for match in matches]
class NotificationTemplate:
"""通知模板类"""
def __init__(self, template_id: str, name: str, template_type: TemplateType,
content: Dict[str, str], variables: List[str],
description: str = "", channel: str = "all"):
"""
初始化通知模板
Args:
template_id: 模板ID
name: 模板名称
template_type: 模板类型
content: 模板内容,字典格式,key为渠道,value为模板字符串
variables: 模板变量列表
description: 模板描述
channel: 适用渠道
"""
self.template_id = template_id
self.name = name
self.template_type = template_type
self.content = content
self.variables = variables
self.description = description
self.channel = channel
self.created_at = datetime.now()
self.updated_at = datetime.now()
self.version = 1
# 初始化模板引擎
self.engine = SimpleTemplateEngine()
def render(self, channel: str, context: Dict[str, Any]) -> str:
"""渲染模板
Args:
channel: 渠道名称
context: 变量上下文
Returns:
渲染后的内容
"""
# 获取对应渠道的模板
template_content = self.content.get(channel)
if not template_content:
# 如果没有特定渠道的模板,使用默认模板
template_content = self.content.get("default")
if not template_content:
raise ValueError(f"模板 {self.name} 没有为渠道 {channel} 提供内容")
# 渲染模板
return self.engine.render(template_content, context)
def validate(self) -> Tuple[bool, Dict[str, List[str]]]:
"""验证模板
Returns:
(是否有效, 错误字典)
"""
errors = {}
for channel, content in self.content.items():
is_valid, channel_errors = self.engine.validate(content)
if not is_valid:
errors[channel] = channel_errors
return len(errors) == 0, errors
def update_content(self, new_content: Dict[str, str],
description: str = "") -> bool:
"""更新模板内容
Args:
new_content: 新的模板内容
description: 更新描述
Returns:
是否更新成功
"""
# 验证新内容
for channel, content in new_content.items():
is_valid, errors = self.engine.validate(content)
if not is_valid:
logger.error(f"模板内容验证失败: {errors}")
return False
# 提取新变量
new_variables = set()
for content in new_content.values():
new_variables.update(self.engine.extract_variables(content))
# 更新模板
self.content = new_content
self.variables = list(new_variables)
if description:
self.description = description
self.updated_at = datetime.now()
self.version += 1
return True
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"template_id": self.template_id,
"name": self.name,
"type": self.template_type.value,
"content": self.content,
"variables": self.variables,
"description": self.description,
"channel": self.channel,
"version": self.version,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat()
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'NotificationTemplate':
"""从字典创建模板对象"""
template = cls(
template_id=data["template_id"],
name=data["name"],
template_type=TemplateType(data["type"]),
content=data["content"],
variables=data["variables"],
description=data.get("description", ""),
channel=data.get("channel", "all")
)
template.version = data.get("version", 1)
template.created_at = datetime.fromisoformat(data["created_at"])
template.updated_at = datetime.fromisoformat(data["updated_at"])
return template
class TemplateManager:
"""模板管理器"""
def __init__(self, storage_backend=None):
"""
初始化模板管理器
Args:
storage_backend: 存储后端,默认为内存存储
"""
self.storage = storage_backend or InMemoryTemplateStorage()
self.template_cache = {} # 模板缓存
self.engine = SimpleTemplateEngine()
def create_template(self, name: str, template_type: TemplateType,
content: Dict[str, str], description: str = "",
channel: str = "all") -> Tuple[bool, Dict[str, Any]]:
"""创建模板
Args:
name: 模板名称
template_type: 模板类型
content: 模板内容
description: 模板描述
channel: 适用渠道
Returns:
(是否成功, 模板信息或错误信息)
"""
try:
# 验证模板名称唯一性
existing = self.storage.get_template_by_name(name)
if existing:
return False, {"error": f"模板名称 '{name}' 已存在"}
# 提取变量
variables = set()
for template_content in content.values():
variables.update(self.engine.extract_variables(template_content))
# 生成模板ID
template_id = self._generate_template_id(name)
# 创建模板对象
template = NotificationTemplate(
template_id=template_id,
name=name,
template_type=template_type,
content=content,
variables=list(variables),
description=description,
channel=channel
)
# 验证模板
is_valid, errors = template.validate()
if not is_valid:
return False, {"error": "模板验证失败", "details": errors}
# 保存模板
self.storage.save_template(template)
# 更新缓存
self.template_cache[template_id] = template
return True, template.to_dict()
except Exception as e:
logger.error(f"创建模板失败: {str(e)}")
return False, {"error": f"创建模板失败: {str(e)}"}
def get_template(self, template_id: str) -> Optional[NotificationTemplate]:
"""获取模板
Args:
template_id: 模板ID
Returns:
模板对象或None
"""
# 尝试从缓存获取
if template_id in self.template_cache:
return self.template_cache[template_id]
# 从存储获取
template = self.storage.get_template(template_id)
if template:
self.template_cache[template_id] = template
return template
def get_template_by_name(self, name: str) -> Optional[NotificationTemplate]:
"""根据名称获取模板
Args:
name: 模板名称
Returns:
模板对象或None
"""
return self.storage.get_template_by_name(name)
def update_template(self, template_id: str,
updates: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]:
"""更新模板
Args:
template_id: 模板ID
updates: 更新内容
Returns:
(是否成功, 更新后的模板或错误信息)
"""
try:
# 获取模板
template = self.get_template(template_id)
if not template:
return False, {"error": f"模板不存在: {template_id}"}
# 应用更新
updated = False
if "name" in updates and updates["name"] != template.name:
# 检查新名称是否唯一
existing = self.storage.get_template_by_name(updates["name"])
if existing and existing.template_id != template_id:
return False, {"error": f"模板名称 '{updates['name']}' 已存在"}
template.name = updates["name"]
updated = True
if "content" in updates:
description = updates.get("description", "")
success = template.update_content(updates["content"], description)
if not success:
return False, {"error": "模板内容更新失败"}
updated = True
if "description" in updates and "content" not in updates:
template.description = updates["description"]
updated = True
if "channel" in updates:
template.channel = updates["channel"]
updated = True
# 如果有更新,保存模板
if updated:
template.updated_at = datetime.now()
self.storage.save_template(template)
self.template_cache[template_id] = template
return True, template.to_dict()
except Exception as e:
logger.error(f"更新模板失败: {str(e)}")
return False, {"error": f"更新模板失败: {str(e)}"}
def delete_template(self, template_id: str) -> bool:
"""删除模板
Args:
template_id: 模板ID
Returns:
是否删除成功
"""
try:
# 从缓存中删除
if template_id in self.template_cache:
del self.template_cache[template_id]
# 从存储中删除
return self.storage.delete_template(template_id)
except Exception as e:
logger.error(f"删除模板失败: {str(e)}")
return False
def render_template(self, template_id: str, channel: str,
context: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]:
"""渲染模板
Args:
template_id: 模板ID
channel: 渠道名称
context: 变量上下文
Returns:
(是否成功, 渲染结果或错误信息)
"""
try:
# 获取模板
template = self.get_template(template_id)
if not template:
return False, {"error": f"模板不存在: {template_id}"}
# 检查渠道是否支持
if template.channel != "all" and template.channel != channel:
return False, {"error": f"模板不支持渠道: {channel}"}
# 渲染模板
rendered_content = template.render(channel, context)
return True, {
"content": rendered_content,
"template_name": template.name,
"template_type": template.template_type.value
}
except Exception as e:
logger.error(f"渲染模板失败: {str(e)}")
return False, {"error": f"渲染模板失败: {str(e)}"}
def list_templates(self, template_type: Optional[TemplateType] = None,
channel: Optional[str] = None) -> List[Dict[str, Any]]:
"""列出模板
Args:
template_type: 过滤模板类型
channel: 过滤渠道
Returns:
模板列表
"""
templates = self.storage.list_templates()
# 过滤
if template_type:
templates = [t for t in templates if t.template_type == template_type]
if channel:
templates = [t for t in templates if t.channel == "all" or t.channel == channel]
return [template.to_dict() for template in templates]
def _generate_template_id(self, name: str) -> str:
"""生成模板ID
Args:
name: 模板名称
Returns:
模板ID
"""
# 使用名称和时间的哈希作为ID
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
hash_input = f"{name}_{timestamp}"
hash_value = hashlib.md5(hash_input.encode()).hexdigest()[:8]
return f"TPL{timestamp}{hash_value}"
class InMemoryTemplateStorage:
"""内存模板存储(简化实现,实际应该使用数据库)"""
def __init__(self):
self.templates = {} # template_id -> NotificationTemplate
self.name_index = {} # name -> template_id
def save_template(self, template: NotificationTemplate):
"""保存模板"""
self.templates[template.template_id] = template
self.name_index[template.name] = template.template_id
def get_template(self, template_id: str) -> Optional[NotificationTemplate]:
"""获取模板"""
return self.templates.get(template_id)
def get_template_by_name(self, name: str) -> Optional[NotificationTemplate]:
"""根据名称获取模板"""
template_id = self.name_index.get(name)
if template_id:
return self.templates.get(template_id)
return None
def delete_template(self, template_id: str) -> bool:
"""删除模板"""
if template_id in self.templates:
template = self.templates[template_id]
# 从名称索引中删除
if template.name in self.name_index:
del self.name_index[template.name]
# 从模板字典中删除
del self.templates[template_id]
return True
return False
def list_templates(self) -> List[NotificationTemplate]:
"""列出所有模板"""
return list(self.templates.values())
4.2 渠道适配器
python
# channels/channel_adapters.py
import smtplib
import json
import logging
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple, Any
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import ssl
import requests
from jinja2 import Template
logger = logging.getLogger(__name__)
class ChannelType(Enum):
"""渠道类型枚举"""
EMAIL = "email"
SMS = "sms"
PUSH = "push"
IN_APP = "in_app"
WEBHOOK = "webhook"
class NotificationChannel(ABC):
"""通知渠道抽象基类"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
"""
初始化通知渠道
Args:
channel_id: 渠道ID
name: 渠道名称
config: 渠道配置
"""
self.channel_id = channel_id
self.name = name
self.config = config
self.enabled = True
self.priority = 1 # 默认优先级
self.created_at = datetime.now()
self.updated_at = datetime.now()
@abstractmethod
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送通知
Args:
to: 接收者
subject: 主题/标题
content: 内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
pass
@abstractmethod
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置
Returns:
(是否有效, 错误列表)
"""
pass
@abstractmethod
def test_connection(self) -> bool:
"""测试连接
Returns:
连接是否成功
"""
pass
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"channel_id": self.channel_id,
"name": self.name,
"type": self.type().value,
"config": self.config,
"enabled": self.enabled,
"priority": self.priority,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat()
}
@classmethod
@abstractmethod
def type(cls) -> ChannelType:
"""返回渠道类型"""
pass
class EmailChannel(NotificationChannel):
"""邮件渠道"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
super().__init__(channel_id, name, config)
self.smtp_server = config.get("smtp_server", "")
self.smtp_port = config.get("smtp_port", 587)
self.username = config.get("username", "")
self.password = config.get("password", "")
self.use_tls = config.get("use_tls", True)
self.from_email = config.get("from_email", "")
self.from_name = config.get("from_name", "")
@classmethod
def type(cls) -> ChannelType:
return ChannelType.EMAIL
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送邮件
Args:
to: 接收邮箱
subject: 邮件主题
content: 邮件内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
try:
# 创建邮件
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f"{self.from_name} <{self.from_email}>" if self.from_name else self.from_email
msg['To'] = to
# 添加文本和HTML版本
text_part = MIMEText(content, 'plain', 'utf-8')
html_part = MIMEText(content, 'html', 'utf-8') if metadata and metadata.get("is_html") else None
msg.attach(text_part)
if html_part:
msg.attach(html_part)
# 发送邮件
context = ssl.create_default_context() if self.use_tls else None
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
if self.use_tls:
server.starttls(context=context)
if self.username and self.password:
server.login(self.username, self.password)
server.send_message(msg)
logger.info(f"邮件发送成功: {to}")
return True, {
"message_id": f"email_{datetime.now().timestamp()}",
"to": to,
"channel": self.name,
"sent_at": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}")
return False, {
"error": str(e),
"to": to,
"channel": self.name
}
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置"""
errors = []
required_fields = ["smtp_server", "smtp_port", "username", "password", "from_email"]
for field in required_fields:
if not self.config.get(field):
errors.append(f"缺少必要字段: {field}")
if not errors:
# 验证端口号
port = self.config.get("smtp_port")
if not isinstance(port, int) or port <= 0 or port > 65535:
errors.append(f"无效的端口号: {port}")
# 验证发件人邮箱格式
from_email = self.config.get("from_email", "")
if "@" not in from_email:
errors.append(f"无效的发件人邮箱: {from_email}")
return len(errors) == 0, errors
def test_connection(self) -> bool:
"""测试SMTP连接"""
try:
context = ssl.create_default_context() if self.use_tls else None
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
if self.use_tls:
server.starttls(context=context)
if self.username and self.password:
server.login(self.username, self.password)
# 发送NOOP命令测试连接
server.noop()
return True
except Exception as e:
logger.error(f"SMTP连接测试失败: {str(e)}")
return False
class SMSChannel(NotificationChannel):
"""短信渠道"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
super().__init__(channel_id, name, config)
self.api_key = config.get("api_key", "")
self.api_secret = config.get("api_secret", "")
self.api_url = config.get("api_url", "")
self.sender_id = config.get("sender_id", "")
self.signature = config.get("signature", "")
@classmethod
def type(cls) -> ChannelType:
return ChannelType.SMS
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送短信
Args:
to: 接收手机号
subject: 短信主题(某些服务商支持)
content: 短信内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
try:
# 构建请求数据(模拟阿里云短信接口)
request_data = {
"PhoneNumbers": to,
"SignName": self.signature,
"TemplateCode": metadata.get("template_code", "") if metadata else "",
"TemplateParam": json.dumps({"code": content[:6]} if len(content) >= 6 else {"message": content})
}
# 添加认证信息
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 发送请求(模拟)
# response = requests.post(self.api_url, json=request_data, headers=headers)
# response.raise_for_status()
# 模拟成功响应
logger.info(f"短信发送成功: {to}")
return True, {
"message_id": f"sms_{datetime.now().timestamp()}",
"to": to,
"channel": self.name,
"sent_at": datetime.now().isoformat(),
"content_length": len(content)
}
except Exception as e:
logger.error(f"短信发送失败: {str(e)}")
return False, {
"error": str(e),
"to": to,
"channel": self.name
}
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置"""
errors = []
required_fields = ["api_key", "api_secret", "api_url", "signature"]
for field in required_fields:
if not self.config.get(field):
errors.append(f"缺少必要字段: {field}")
return len(errors) == 0, errors
def test_connection(self) -> bool:
"""测试API连接"""
try:
# 模拟API连接测试
# response = requests.get(f"{self.api_url}/test", headers={
# "Authorization": f"Bearer {self.api_key}"
# })
# return response.status_code == 200
# 简化实现
return bool(self.api_key and self.api_secret)
except Exception as e:
logger.error(f"SMS API连接测试失败: {str(e)}")
return False
class PushNotificationChannel(NotificationChannel):
"""推送通知渠道"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
super().__init__(channel_id, name, config)
self.fcm_api_key = config.get("fcm_api_key", "")
self.apns_key_id = config.get("apns_key_id", "")
self.apns_team_id = config.get("apns_team_id", "")
self.apns_bundle_id = config.get("apns_bundle_id", "")
self.apns_key_file = config.get("apns_key_file", "")
@classmethod
def type(cls) -> ChannelType:
return ChannelType.PUSH
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送推送通知
Args:
to: 设备令牌
subject: 通知标题
content: 通知内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
try:
# 判断设备类型(根据令牌格式,简化实现)
device_type = self._detect_device_type(to)
if device_type == "android":
return self._send_fcm(to, subject, content, metadata)
elif device_type == "ios":
return self._send_apns(to, subject, content, metadata)
else:
return False, {"error": f"未知设备类型: {device_type}"}
except Exception as e:
logger.error(f"推送通知发送失败: {str(e)}")
return False, {
"error": str(e),
"to": to,
"channel": self.name
}
def _detect_device_type(self, token: str) -> str:
"""检测设备类型(简化实现)"""
# 实际实现应该根据令牌格式判断
if token.startswith("android_"):
return "android"
elif token.startswith("ios_"):
return "ios"
else:
# 默认为Android
return "android"
def _send_fcm(self, token: str, title: str, body: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送FCM推送"""
try:
# 构建FCM消息
fcm_message = {
"message": {
"token": token,
"notification": {
"title": title,
"body": body
},
"data": metadata.get("data", {}) if metadata else {}
}
}
# 发送请求(模拟)
# headers = {
# "Authorization": f"key={self.fcm_api_key}",
# "Content-Type": "application/json"
# }
# response = requests.post(
# "https://fcm.googleapis.com/fcm/send",
# json=fcm_message,
# headers=headers
# )
# response.raise_for_status()
logger.info(f"FCM推送发送成功: {token}")
return True, {
"message_id": f"fcm_{datetime.now().timestamp()}",
"to": token,
"channel": self.name,
"sent_at": datetime.now().isoformat(),
"platform": "android"
}
except Exception as e:
logger.error(f"FCM推送发送失败: {str(e)}")
return False, {"error": str(e)}
def _send_apns(self, token: str, title: str, body: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送APNS推送"""
try:
# 构建APNS消息(模拟)
apns_message = {
"aps": {
"alert": {
"title": title,
"body": body
},
"badge": metadata.get("badge", 1) if metadata else 1,
"sound": metadata.get("sound", "default") if metadata else "default"
}
}
# 添加自定义数据
if metadata and "data" in metadata:
apns_message.update(metadata["data"])
# 发送请求(模拟)
logger.info(f"APNS推送发送成功: {token}")
return True, {
"message_id": f"apns_{datetime.now().timestamp()}",
"to": token,
"channel": self.name,
"sent_at": datetime.now().isoformat(),
"platform": "ios"
}
except Exception as e:
logger.error(f"APNS推送发送失败: {str(e)}")
return False, {"error": str(e)}
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置"""
errors = []
# 至少需要配置一种推送服务
if not self.fcm_api_key and not (self.apns_key_id and self.apns_team_id):
errors.append("需要配置至少一种推送服务 (FCM 或 APNS)")
# 如果配置了APNS,检查必要字段
if self.apns_key_id:
required_fields = ["apns_key_id", "apns_team_id", "apns_bundle_id", "apns_key_file"]
for field in required_fields:
if not self.config.get(field):
errors.append(f"APNS配置缺少必要字段: {field}")
return len(errors) == 0, errors
def test_connection(self) -> bool:
"""测试连接"""
# 简化实现
return bool(self.fcm_api_key or self.apns_key_id)
class InAppChannel(NotificationChannel):
"""站内信渠道"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
super().__init__(channel_id, name, config)
self.storage_backend = config.get("storage_backend", "memory")
@classmethod
def type(cls) -> ChannelType:
return ChannelType.IN_APP
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送站内信
Args:
to: 用户ID
subject: 消息标题
content: 消息内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
try:
# 在实际系统中,这里应该将消息保存到数据库
message_data = {
"user_id": to,
"title": subject,
"content": content,
"metadata": metadata or {},
"created_at": datetime.now().isoformat(),
"read": False
}
# 模拟保存到数据库
logger.info(f"站内信保存成功: {to}")
return True, {
"message_id": f"inapp_{datetime.now().timestamp()}",
"to": to,
"channel": self.name,
"sent_at": datetime.now().isoformat(),
"saved": True
}
except Exception as e:
logger.error(f"站内信发送失败: {str(e)}")
return False, {
"error": str(e),
"to": to,
"channel": self.name
}
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置"""
# 站内信渠道通常不需要额外配置
return True, []
def test_connection(self) -> bool:
"""测试连接"""
# 站内信渠道总是可用的
return True
class WebhookChannel(NotificationChannel):
"""Webhook渠道"""
def __init__(self, channel_id: str, name: str, config: Dict[str, Any]):
super().__init__(channel_id, name, config)
self.webhook_url = config.get("webhook_url", "")
self.secret_key = config.get("secret_key", "")
self.headers = config.get("headers", {})
@classmethod
def type(cls) -> ChannelType:
return ChannelType.WEBHOOK
def send(self, to: str, subject: str, content: str,
metadata: Optional[Dict] = None) -> Tuple[bool, Dict[str, Any]]:
"""发送Webhook
Args:
to: Webhook URL(如果配置中未指定)
subject: 事件类型
content: 消息内容
metadata: 元数据
Returns:
(是否成功, 发送结果)
"""
try:
# 使用配置的URL或传入的URL
url = to if to.startswith("http") else self.webhook_url
# 构建请求数据
request_data = {
"event": subject,
"data": content if isinstance(content, dict) else {"message": content},
"timestamp": datetime.now().isoformat(),
"metadata": metadata or {}
}
# 添加签名
if self.secret_key:
import hmac
import hashlib
signature = hmac.new(
self.secret_key.encode(),
json.dumps(request_data).encode(),
hashlib.sha256
).hexdigest()
request_data["signature"] = signature
# 发送请求(模拟)
# headers = {"Content-Type": "application/json"}
# headers.update(self.headers)
# response = requests.post(url, json=request_data, headers=headers)
# response.raise_for_status()
logger.info(f"Webhook发送成功: {url}")
return True, {
"message_id": f"webhook_{datetime.now().timestamp()}",
"to": url,
"channel": self.name,
"sent_at": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Webhook发送失败: {str(e)}")
return False, {
"error": str(e),
"to": to,
"channel": self.name
}
def validate_config(self) -> Tuple[bool, List[str]]:
"""验证配置"""
errors = []
if not self.webhook_url:
errors.append("缺少webhook_url配置")
return len(errors) == 0, errors
def test_connection(self) -> bool:
"""测试连接"""
try:
# 测试URL是否可达
# response = requests.get(self.webhook_url, timeout=5)
# return response.status_code < 400
# 简化实现
return bool(self.webhook_url)
except Exception as e:
logger.error(f"Webhook连接测试失败: {str(e)}")
return False
class ChannelManager:
"""渠道管理器"""
def __init__(self):
self.channels = {} # channel_id -> NotificationChannel
self.channel_types = {
ChannelType.EMAIL: EmailChannel,
ChannelType.SMS: SMSChannel,
ChannelType.PUSH: PushNotificationChannel,
ChannelType.IN_APP: InAppChannel,
ChannelType.WEBHOOK: WebhookChannel
}
def register_channel(self, channel: NotificationChannel):
"""注册渠道"""
self.channels[channel.channel_id] = channel
def create_channel(self, name: str, channel_type: ChannelType,
config: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]:
"""创建渠道
Args:
name: 渠道名称
channel_type: 渠道类型
config: 渠道配置
Returns:
(是否成功, 渠道信息或错误信息)
"""
try:
# 检查渠道类型是否支持
if channel_type not in self.channel_types:
return False, {"error": f"不支持的渠道类型: {channel_type}"}
# 生成渠道ID
channel_id = self._generate_channel_id(name, channel_type)
# 创建渠道实例
channel_class = self.channel_types[channel_type]
channel = channel_class(channel_id, name, config)
# 验证配置
is_valid, errors = channel.validate_config()
if not is_valid:
return False, {"error": "渠道配置验证失败", "details": errors}
# 注册渠道
self.register_channel(channel)
return True, channel.to_dict()
except Exception as e:
logger.error(f"创建渠道失败: {str(e)}")
return False, {"error": f"创建渠道失败: {str(e)}"}
def get_channel(self, channel_id: str) -> Optional[NotificationChannel]:
"""获取渠道"""
return self.channels.get(channel_id)
def get_channels_by_type(self, channel_type: ChannelType) -> List[NotificationChannel]:
"""根据类型获取渠道"""
return [channel for channel in self.channels.values()
if channel.type() == channel_type]
def get_available_channels(self, notification_type: str) -> List[NotificationChannel]:
"""获取可用的渠道(根据通知类型)
Args:
notification_type: 通知类型
Returns:
可用的渠道列表
"""
# 简化实现:返回所有启用的渠道
# 实际实现应该根据通知类型和渠道能力进行匹配
return [channel for channel in self.channels.values()
if channel.enabled]
def update_channel(self, channel_id: str,
updates: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]:
"""更新渠道
Args:
channel_id: 渠道ID
updates: 更新内容
Returns:
(是否成功, 更新后的渠道或错误信息)
"""
try:
# 获取渠道
channel = self.get_channel(channel_id)
if not channel:
return False, {"error": f"渠道不存在: {channel_id}"}
# 应用更新
if "name" in updates:
channel.name = updates["name"]
if "config" in updates:
# 更新配置
channel.config.update(updates["config"])
# 重新验证配置
is_valid, errors = channel.validate_config()
if not is_valid:
return False, {"error": "配置验证失败", "details": errors}
if "enabled" in updates:
channel.enabled = updates["enabled"]
if "priority" in updates:
channel.priority = updates["priority"]
channel.updated_at = datetime.now()
return True, channel.to_dict()
except Exception as e:
logger.error(f"更新渠道失败: {str(e)}")
return False, {"error": f"更新渠道失败: {str(e)}"}
def delete_channel(self, channel_id: str) -> bool:
"""删除渠道
Args:
channel_id: 渠道ID
Returns:
是否删除成功
"""
if channel_id in self.channels:
del self.channels[channel_id]
return True
return False
def test_channel(self, channel_id: str) -> Tuple[bool, Dict[str, Any]]:
"""测试渠道连接
Args:
channel_id: 渠道ID
Returns:
(是否成功, 测试结果)
"""
channel = self.get_channel(channel_id)
if not channel:
return False, {"error": f"渠道不存在: {channel_id}"}
try:
success = channel.test_connection()
if success:
return True, {"message": "渠道连接测试成功"}
else:
return False, {"error": "渠道连接测试失败"}
except Exception as e:
logger.error(f"渠道测试异常: {str(e)}")
return False, {"error": f"渠道测试异常: {str(e)}"}
def list_channels(self, channel_type: Optional[ChannelType] = None) -> List[Dict[str, Any]]:
"""列出渠道
Args:
channel_type: 过滤渠道类型
Returns:
渠道列表
"""
channels = list(self.channels.values())
if channel_type:
channels = [channel for channel in channels
if channel.type() == channel_type]
# 按优先级排序
channels.sort(key=lambda x: x.priority)
return [channel.to_dict() for channel in channels]
def _generate_channel_id(self, name: str, channel_type: ChannelType) -> str:
"""生成渠道ID
Args:
name: 渠道名称
channel_type: 渠道类型
Returns:
渠道ID
"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
type_code = channel_type.value[:3].upper()
name_code = name[:3].upper()
return f"CHN{type_code}{name_code}{timestamp}"
4.3 通知服务核心
python
# core/notification_service.py
import json
import logging
import threading
import time
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
from typing import Dict, List, Optional, Tuple, Any, Callable
from uuid import uuid4
from channels.channel_adapters import ChannelManager, ChannelType, NotificationChannel
from templates.template_manager import TemplateManager, TemplateType
logger = logging.getLogger(__name__)
class NotificationStatus(Enum):
"""通知状态枚举"""
PENDING = "pending" # 待发送
PROCESSING = "processing" # 处理中
SENT = "sent" # 已发送
FAILED = "failed" # 发送失败
RETRYING = "retrying" # 重试中
CANCELLED = "cancelled" # 已取消
class NotificationPriority(Enum):
"""通知优先级枚举"""
LOW = 0 # 低优先级
NORMAL = 1 # 正常优先级
HIGH = 2 # 高优先级
URGENT = 3 # 紧急优先级
class Notification:
"""通知类"""
def __init__(self, notification_id: str, user_id: str,
template_id: str, context: Dict[str, Any],
channels: List[str], priority: NotificationPriority = NotificationPriority.NORMAL):
"""
初始化通知
Args:
notification_id: 通知ID
user_id: 用户ID
template_id: 模板ID
context: 模板上下文
channels: 目标渠道列表
priority: 优先级
"""
self.notification_id = notification_id
self.user_id = user_id
self.template_id = template_id
self.context = context
self.channels = channels
self.priority = priority
# 状态信息
self.status = NotificationStatus.PENDING
self.created_at = datetime.now()
self.updated_at = datetime.now()
self.scheduled_at = None
self.sent_at = None
# 发送结果
self.send_results = {} # channel_id -> result
self.retry_count = 0
self.max_retries = 3
# 元数据
self.metadata = {}
def update_status(self, new_status: NotificationStatus,
result: Optional[Dict] = None,
channel_id: Optional[str] = None):
"""更新状态
Args:
new_status: 新状态
result: 发送结果
channel_id: 渠道ID
"""
self.status = new_status
self.updated_at = datetime.now()
if result and channel_id:
self.send_results[channel_id] = result
# 如果所有渠道都发送完成,更新总体状态
if new_status == NotificationStatus.SENT:
self.sent_at = datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"notification_id": self.notification_id,
"user_id": self.user_id,
"template_id": self.template_id,
"context": self.context,
"channels": self.channels,
"priority": self.priority.value,
"status": self.status.value,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
"sent_at": self.sent_at.isoformat() if self.sent_at else None,
"send_results": self.send_results,
"retry_count": self.retry_count,
"metadata": self.metadata
}
class NotificationQueue(ABC):
"""通知队列抽象基类"""
@abstractmethod
def push(self, notification: Notification):
"""推送通知到队列"""
pass
@abstractmethod
def pop(self) -> Optional[Notification]:
"""从队列弹出通知"""
pass
@abstractmethod
def size(self) -> int:
"""获取队列大小"""
pass
@abstractmethod
def clear(self):
"""清空队列"""
pass
class MemoryNotificationQueue(NotificationQueue):
"""内存通知队列"""
def __init__(self):
self.queue = [] # 使用列表作为队列
self.lock = threading.RLock()
def push(self, notification: Notification):
"""推送通知到队列"""
with self.lock:
# 根据优先级插入队列
priority_value = notification.priority.value
inserted = False
for i, item in enumerate(self.queue):
if item.priority.value < priority_value:
self.queue.insert(i, notification)
inserted = True
break
if not inserted:
self.queue.append(notification)
def pop(self) -> Optional[Notification]:
"""从队列弹出通知"""
with self.lock:
if self.queue:
return self.queue.pop(0)
return None
def size(self) -> int:
"""获取队列大小"""
with self.lock:
return len(self.queue)
def clear(self):
"""清空队列"""
with self.lock:
self.queue.clear()
class UserPreferenceManager:
"""用户偏好管理器"""
def __init__(self, storage_backend=None):
"""
初始化用户偏好管理器
Args:
storage_backend: 存储后端
"""
self.storage = storage_backend or InMemoryUserPreferenceStorage()
def get_user_preferences(self, user_id: str) -> Dict[str, Any]:
"""获取用户偏好
Args:
user_id: 用户ID
Returns:
用户偏好配置
"""
return self.storage.get_preferences(user_id)
def update_user_preferences(self, user_id: str,
preferences: Dict[str, Any]) -> bool:
"""更新用户偏好
Args:
user_id: 用户ID
preferences: 偏好配置
Returns:
是否成功
"""
return self.storage.update_preferences(user_id, preferences)
def get_user_channels(self, user_id: str,
notification_type: str) -> List[str]:
"""获取用户偏好的渠道
Args:
user_id: 用户ID
notification_type: 通知类型
Returns:
渠道列表
"""
preferences = self.get_user_preferences(user_id)
# 获取该通知类型的渠道配置
type_prefs = preferences.get("notification_types", {}).get(notification_type, {})
# 返回启用的渠道
enabled_channels = []
for channel_id, config in type_prefs.items():
if config.get("enabled", True):
enabled_channels.append(channel_id)
return enabled_channels
class InMemoryUserPreferenceStorage:
"""内存用户偏好存储"""
def __init__(self):
self.preferences = {} # user_id -> preferences
def get_preferences(self, user_id: str) -> Dict[str, Any]:
"""获取用户偏好"""
return self.preferences.get(user_id, {
"notification_types": {
"order": {
"email": {"enabled": True},
"sms": {"enabled": False},
"in_app": {"enabled": True}
},
"payment": {
"email": {"enabled": True},
"sms": {"enabled": True},
"in_app": {"enabled": True}
},
"promotion": {
"email": {"enabled": False},
"sms": {"enabled": False},
"in_app": {"enabled": True}
}
},
"global_settings": {
"quiet_hours": {"start": "22:00", "end": "08:00"},
"language": "zh-CN"
}
})
def update_preferences(self, user_id: str,
preferences: Dict[str, Any]) -> bool:
"""更新用户偏好"""
self.preferences[user_id] = preferences
return True
class NotificationService:
"""通知服务"""
def __init__(self, template_manager: TemplateManager,
channel_manager: ChannelManager,
user_preference_manager: UserPreferenceManager,
queue: Optional[NotificationQueue] = None):
"""
初始化通知服务
Args:
template_manager: 模板管理器
channel_manager: 渠道管理器
user_preference_manager: 用户偏好管理器
queue: 通知队列
"""
self.template_manager = template_manager
self.channel_manager = channel_manager
self.user_preference_manager = user_preference_manager
self.queue = queue or MemoryNotificationQueue()
# 发送记录
self.notification_store = {} # notification_id -> Notification
# 启动处理线程
self.processor_thread = threading.Thread(target=self._process_queue, daemon=True)
self.running = False
# 重试队列
self.retry_queue = []
self.retry_lock = threading.RLock()
# 统计信息
self.stats = {
"total_sent": 0,
"total_failed": 0,
"total_retried": 0,
"by_channel": {},
"by_template": {}
}
self.stats_lock = threading.RLock()
def start(self):
"""启动通知服务"""
if not self.running:
self.running = True
self.processor_thread.start()
logger.info("通知服务已启动")
def stop(self):
"""停止通知服务"""
self.running = False
if self.processor_thread.is_alive():
self.processor_thread.join(timeout=5)
logger.info("通知服务已停止")
def send_notification(self, user_id: str, template_name: str,
context: Dict[str, Any],
channels: Optional[List[str]] = None,
priority: NotificationPriority = NotificationPriority.NORMAL,
notification_type: str = "general") -> Tuple[bool, Dict[str, Any]]:
"""发送通知
Args:
user_id: 用户ID
template_name: 模板名称
context: 模板上下文
channels: 目标渠道列表(如果为None,则使用用户偏好)
priority: 优先级
notification_type: 通知类型
Returns:
(是否成功, 通知信息或错误信息)
"""
try:
# 获取模板
template = self.template_manager.get_template_by_name(template_name)
if not template:
return False, {"error": f"模板不存在: {template_name}"}
# 获取用户偏好的渠道
if channels is None:
channels = self.user_preference_manager.get_user_channels(user_id, notification_type)
if not channels:
return False, {"error": "没有可用的发送渠道"}
# 生成通知ID
notification_id = self._generate_notification_id()
# 创建通知对象
notification = Notification(
notification_id=notification_id,
user_id=user_id,
template_id=template.template_id,
context=context,
channels=channels,
priority=priority
)
# 设置元数据
notification.metadata = {
"template_name": template_name,
"notification_type": notification_type,
"user_preferences_used": channels is None
}
# 添加到队列
self.queue.push(notification)
# 保存到存储
self.notification_store[notification_id] = notification
logger.info(f"通知已加入队列: {notification_id}")
return True, {
"notification_id": notification_id,
"status": notification.status.value,
"channels": channels,
"queued_at": notification.created_at.isoformat()
}
except Exception as e:
logger.error(f"发送通知失败: {str(e)}")
return False, {"error": f"发送通知失败: {str(e)}"}
def send_immediate_notification(self, user_id: str, template_name: str,
context: Dict[str, Any],
channels: Optional[List[str]] = None,
priority: NotificationPriority = NotificationPriority.NORMAL) -> Tuple[bool, Dict[str, Any]]:
"""立即发送通知(同步)
Args:
user_id: 用户ID
template_name: 模板名称
context: 模板上下文
channels: 目标渠道列表
priority: 优先级
Returns:
(是否成功, 发送结果)
"""
try:
# 获取模板
template = self.template_manager.get_template_by_name(template_name)
if not template:
return False, {"error": f"模板不存在: {template_name}"}
# 如果没有指定渠道,使用所有可用渠道
if channels is None:
available_channels = self.channel_manager.get_available_channels("general")
channels = [channel.channel_id for channel in available_channels]
results = {}
all_success = True
# 逐个渠道发送
for channel_id in channels:
channel = self.channel_manager.get_channel(channel_id)
if not channel:
results[channel_id] = {"success": False, "error": "渠道不存在"}
all_success = False
continue
if not channel.enabled:
results[channel_id] = {"success": False, "error": "渠道未启用"}
all_success = False
continue
# 渲染模板
success, rendered = self.template_manager.render_template(
template.template_id,
channel.type().value,
context
)
if not success:
results[channel_id] = {"success": False, "error": f"模板渲染失败: {rendered.get('error')}"}
all_success = False
continue
# 发送通知
success, send_result = channel.send(
to=self._get_user_address(user_id, channel.type()),
subject=rendered.get("template_name", "通知"),
content=rendered["content"],
metadata={
"template_name": template_name,
"user_id": user_id,
"is_html": channel.type() == ChannelType.EMAIL
}
)
results[channel_id] = {
"success": success,
"result": send_result if success else {"error": send_result.get("error")}
}
if not success:
all_success = False
# 更新统计
self._update_stats(channel.type().value, success)
return all_success, {
"success": all_success,
"results": results,
"sent_at": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"立即发送通知失败: {str(e)}")
return False, {"error": f"立即发送通知失败: {str(e)}"}
def schedule_notification(self, user_id: str, template_name: str,
context: Dict[str, Any], send_time: datetime,
channels: Optional[List[str]] = None,
priority: NotificationPriority = NotificationPriority.NORMAL) -> Tuple[bool, Dict[str, Any]]:
"""计划发送通知
Args:
user_id: 用户ID
template_name: 模板名称
context: 模板上下文
send_time: 发送时间
channels: 目标渠道列表
priority: 优先级
Returns:
(是否成功, 通知信息)
"""
try:
# 检查发送时间
if send_time < datetime.now():
return False, {"error": "发送时间不能早于当前时间"}
# 获取模板
template = self.template_manager.get_template_by_name(template_name)
if not template:
return False, {"error": f"模板不存在: {template_name}"}
# 生成通知ID
notification_id = self._generate_notification_id()
# 创建通知对象
notification = Notification(
notification_id=notification_id,
user_id=user_id,
template_id=template.template_id,
context=context,
channels=channels or [],
priority=priority
)
# 设置计划时间
notification.scheduled_at = send_time
notification.status = NotificationStatus.PENDING
# 保存到存储
self.notification_store[notification_id] = notification
# 如果发送时间很近,直接加入队列
if (send_time - datetime.now()).total_seconds() < 60: # 1分钟内
self.queue.push(notification)
else:
# 否则加入计划队列,稍后处理
self._add_to_scheduled_queue(notification)
logger.info(f"计划通知已创建: {notification_id}, 计划时间: {send_time}")
return True, {
"notification_id": notification_id,
"scheduled_at": send_time.isoformat(),
"status": notification.status.value
}
except Exception as e:
logger.error(f"计划通知失败: {str(e)}")
return False, {"error": f"计划通知失败: {str(e)}"}
def get_notification_status(self, notification_id: str) -> Optional[Dict[str, Any]]:
"""获取通知状态
Args:
notification_id: 通知ID
Returns:
通知状态信息
"""
notification = self.notification_store.get(notification_id)
if notification:
return notification.to_dict()
return None
def cancel_notification(self, notification_id: str) -> bool:
"""取消通知
Args:
notification_id: 通知ID
Returns:
是否成功
"""
notification = self.notification_store.get(notification_id)
if notification:
notification.update_status(NotificationStatus.CANCELLED)
logger.info(f"通知已取消: {notification_id}")
return True
return False
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息
Returns:
统计信息
"""
with self.stats_lock:
return self.stats.copy()
def _process_queue(self):
"""处理队列中的通知"""
logger.info("通知队列处理器已启动")
while self.running:
try:
# 检查计划通知
self._check_scheduled_notifications()
# 检查重试队列
self._check_retry_queue()
# 处理队列中的通知
notification = self.queue.pop()
if notification:
self._process_notification(notification)
else:
# 队列为空,休眠一会儿
time.sleep(0.1)
except Exception as e:
logger.error(f"队列处理异常: {str(e)}")
time.sleep(1)
def _process_notification(self, notification: Notification):
"""处理通知
Args:
notification: 通知对象
"""
try:
# 更新状态为处理中
notification.update_status(NotificationStatus.PROCESSING)
# 获取模板
template = self.template_manager.get_template(notification.template_id)
if not template:
logger.error(f"模板不存在: {notification.template_id}")
notification.update_status(NotificationStatus.FAILED)
return
# 发送到各个渠道
all_success = True
results = {}
for channel_id in notification.channels:
channel = self.channel_manager.get_channel(channel_id)
if not channel:
results[channel_id] = {"success": False, "error": "渠道不存在"}
all_success = False
continue
if not channel.enabled:
results[channel_id] = {"success": False, "error": "渠道未启用"}
all_success = False
continue
# 渲染模板
success, rendered = self.template_manager.render_template(
notification.template_id,
channel.type().value,
notification.context
)
if not success:
results[channel_id] = {"success": False, "error": f"模板渲染失败: {rendered.get('error')}"}
all_success = False
continue
# 发送通知
success, send_result = channel.send(
to=self._get_user_address(notification.user_id, channel.type()),
subject=rendered.get("template_name", "通知"),
content=rendered["content"],
metadata={
"template_name": template.name,
"user_id": notification.user_id,
"notification_id": notification.notification_id,
"is_html": channel.type() == ChannelType.EMAIL
}
)
results[channel_id] = {
"success": success,
"result": send_result if success else {"error": send_result.get("error")}
}
if success:
notification.update_status(NotificationStatus.SENT, send_result, channel_id)
else:
all_success = False
notification.update_status(NotificationStatus.FAILED, send_result, channel_id)
# 更新统计
self._update_stats(channel.type().value, success)
# 如果有失败,加入重试队列
if not all_success and notification.retry_count < notification.max_retries:
notification.retry_count += 1
notification.status = NotificationStatus.RETRYING
# 计算重试延迟(指数退避)
retry_delay = min(2 ** notification.retry_count * 60, 3600) # 最大1小时
retry_time = datetime.now() + timedelta(seconds=retry_delay)
with self.retry_lock:
self.retry_queue.append((retry_time, notification))
logger.info(f"通知加入重试队列: {notification.notification_id}, 重试次数: {notification.retry_count}")
elif not all_success:
# 超过最大重试次数,标记为失败
notification.status = NotificationStatus.FAILED
logger.error(f"通知发送失败,超过最大重试次数: {notification.notification_id}")
# 更新统计
with self.stats_lock:
if all_success:
self.stats["total_sent"] += 1
else:
self.stats["total_failed"] += 1
except Exception as e:
logger.error(f"处理通知异常: {notification.notification_id}, 错误: {str(e)}")
notification.update_status(NotificationStatus.FAILED)
def _check_scheduled_notifications(self):
"""检查计划通知"""
# 简化实现,实际应该从数据库查询
current_time = datetime.now()
# 检查存储中的计划通知
for notification in list(self.notification_store.values()):
if (notification.scheduled_at and
notification.status == NotificationStatus.PENDING and
notification.scheduled_at <= current_time):
# 加入处理队列
self.queue.push(notification)
notification.scheduled_at = None
def _check_retry_queue(self):
"""检查重试队列"""
current_time = datetime.now()
with self.retry_lock:
# 找出需要重试的通知
to_retry = []
remaining = []
for retry_time, notification in self.retry_queue:
if retry_time <= current_time:
to_retry.append(notification)
else:
remaining.append((retry_time, notification))
# 更新重试队列
self.retry_queue = remaining
# 将需要重试的通知加入处理队列
for notification in to_retry:
self.queue.push(notification)
with self.stats_lock:
self.stats["total_retried"] += 1
def _add_to_scheduled_queue(self, notification: Notification):
"""添加到计划队列(简化实现)"""
# 在实际系统中,应该使用数据库或专门的调度服务
pass
def _get_user_address(self, user_id: str, channel_type: ChannelType) -> str:
"""获取用户的联系方式
Args:
user_id: 用户ID
channel_type: 渠道类型
Returns:
用户的联系方式
"""
# 简化实现,实际应该从用户服务获取
if channel_type == ChannelType.EMAIL:
return f"user{user_id}@example.com"
elif channel_type == ChannelType.SMS:
return f"13800138{user_id[-4:]}"
elif channel_type == ChannelType.PUSH:
return f"device_token_{user_id}"
elif channel_type == ChannelType.IN_APP:
return user_id
elif channel_type == ChannelType.WEBHOOK:
return f"https://webhook.example.com/user/{user_id}"
else:
return user_id
def _generate_notification_id(self) -> str:
"""生成通知ID"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
random_str = str(uuid4())[:8]
return f"NOT{timestamp}{random_str}"
def _update_stats(self, channel_type: str, success: bool):
"""更新统计信息
Args:
channel_type: 渠道类型
success: 是否成功
"""
with self.stats_lock:
# 更新渠道统计
if channel_type not in self.stats["by_channel"]:
self.stats["by_channel"][channel_type] = {
"sent": 0,
"failed": 0
}
if success:
self.stats["by_channel"][channel_type]["sent"] += 1
else:
self.stats["by_channel"][channel_type]["failed"] += 1
4.4 API服务
python
# api/app.py
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
import logging
from core.notification_service import (
NotificationService, NotificationPriority,
TemplateManager, ChannelManager, UserPreferenceManager
)
from channels.channel_adapters import ChannelType
from templates.template_manager import TemplateType
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 创建FastAPI应用
app = FastAPI(
title="消息通知系统API",
description="企业级消息通知系统,支持多种通知渠道",
version="1.0.0"
)
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 创建服务实例
template_manager = TemplateManager()
channel_manager = ChannelManager()
user_preference_manager = UserPreferenceManager()
notification_service = NotificationService(
template_manager=template_manager,
channel_manager=channel_manager,
user_preference_manager=user_preference_manager
)
# 启动通知服务
notification_service.start()
# 数据模型
class SendNotificationRequest(BaseModel):
"""发送通知请求"""
user_id: str = Field(..., description="用户ID")
template_name: str = Field(..., description="模板名称")
context: Dict[str, Any] = Field(default={}, description="模板上下文")
channels: Optional[List[str]] = Field(None, description="目标渠道列表")
priority: Optional[str] = Field("normal", description="优先级")
notification_type: Optional[str] = Field("general", description="通知类型")
immediate: Optional[bool] = Field(False, description="是否立即发送")
class ScheduleNotificationRequest(BaseModel):
"""计划发送通知请求"""
user_id: str = Field(..., description="用户ID")
template_name: str = Field(..., description="模板名称")
context: Dict[str, Any] = Field(default={}, description="模板上下文")
send_time: str = Field(..., description="发送时间(ISO格式)")
channels: Optional[List[str]] = Field(None, description="目标渠道列表")
priority: Optional[str] = Field("normal", description="优先级")
class CreateTemplateRequest(BaseModel):
"""创建模板请求"""
name: str = Field(..., description="模板名称")
type: str = Field(..., description="模板类型")
content: Dict[str, str] = Field(..., description="模板内容")
description: Optional[str] = Field("", description="模板描述")
channel: Optional[str] = Field("all", description="适用渠道")
class CreateChannelRequest(BaseModel):
"""创建渠道请求"""
name: str = Field(..., description="渠道名称")
type: str = Field(..., description="渠道类型")
config: Dict[str, Any] = Field(..., description="渠道配置")
class UpdateUserPreferencesRequest(BaseModel):
"""更新用户偏好请求"""
notification_types: Dict[str, Dict[str, Dict[str, Any]]] = Field(
default={},
description="通知类型配置"
)
global_settings: Dict[str, Any] = Field(
default={},
description="全局设置"
)
# API端点
@app.get("/")
async def root():
"""根端点"""
return {
"service": "消息通知系统",
"version": "1.0.0",
"status": "运行中"
}
@app.get("/health")
async def health_check():
"""健康检查"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"queue_size": notification_service.queue.size()
}
@app.post("/notifications/send", response_model=Dict[str, Any])
async def send_notification(request: SendNotificationRequest, background_tasks: BackgroundTasks):
"""发送通知"""
try:
# 转换优先级
priority_map = {
"low": NotificationPriority.LOW,
"normal": NotificationPriority.NORMAL,
"high": NotificationPriority.HIGH,
"urgent": NotificationPriority.URGENT
}
priority = priority_map.get(request.priority.lower(), NotificationPriority.NORMAL)
if request.immediate:
# 立即发送
success, result = notification_service.send_immediate_notification(
user_id=request.user_id,
template_name=request.template_name,
context=request.context,
channels=request.channels,
priority=priority
)
else:
# 异步发送
success, result = notification_service.send_notification(
user_id=request.user_id,
template_name=request.template_name,
context=request.context,
channels=request.channels,
priority=priority,
notification_type=request.notification_type
)
if not success:
raise HTTPException(status_code=400, detail=result.get("error", "发送失败"))
return result
except Exception as e:
logger.error(f"发送通知失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"发送通知失败: {str(e)}")
@app.post("/notifications/schedule", response_model=Dict[str, Any])
async def schedule_notification(request: ScheduleNotificationRequest):
"""计划发送通知"""
try:
# 解析发送时间
send_time = datetime.fromisoformat(request.send_time)
# 转换优先级
priority_map = {
"low": NotificationPriority.LOW,
"normal": NotificationPriority.NORMAL,
"high": NotificationPriority.HIGH,
"urgent": NotificationPriority.URGENT
}
priority = priority_map.get(request.priority.lower(), NotificationPriority.NORMAL)
success, result = notification_service.schedule_notification(
user_id=request.user_id,
template_name=request.template_name,
context=request.context,
send_time=send_time,
channels=request.channels,
priority=priority
)
if not success:
raise HTTPException(status_code=400, detail=result.get("error", "计划失败"))
return result
except ValueError:
raise HTTPException(status_code=400, detail="无效的时间格式")
except Exception as e:
logger.error(f"计划通知失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"计划通知失败: {str(e)}")
@app.get("/notifications/{notification_id}", response_model=Dict[str, Any])
async def get_notification_status(notification_id: str):
"""获取通知状态"""
status = notification_service.get_notification_status(notification_id)
if not status:
raise HTTPException(status_code=404, detail="通知不存在")
return status
@app.delete("/notifications/{notification_id}")
async def cancel_notification(notification_id: str):
"""取消通知"""
success = notification_service.cancel_notification(notification_id)
if not success:
raise HTTPException(status_code=404, detail="通知不存在")
return {"message": "通知已取消"}
@app.post("/templates", response_model=Dict[str, Any])
async def create_template(request: CreateTemplateRequest):
"""创建模板"""
try:
# 转换模板类型
template_type = TemplateType(request.type.lower())
success, result = template_manager.create_template(
name=request.name,
template_type=template_type,
content=request.content,
description=request.description,
channel=request.channel
)
if not success:
raise HTTPException(status_code=400, detail=result.get("error", "创建失败"))
return result
except ValueError:
raise HTTPException(status_code=400, detail=f"无效的模板类型: {request.type}")
except Exception as e:
logger.error(f"创建模板失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"创建模板失败: {str(e)}")
@app.get("/templates", response_model=List[Dict[str, Any]])
async def list_templates(type: Optional[str] = None, channel: Optional[str] = None):
"""列出模板"""
try:
template_type = TemplateType(type.lower()) if type else None
templates = template_manager.list_templates(template_type, channel)
return templates
except ValueError:
raise HTTPException(status_code=400, detail=f"无效的模板类型: {type}")
except Exception as e:
logger.error(f"获取模板列表失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取模板列表失败: {str(e)}")
@app.get("/templates/{template_id}", response_model=Dict[str, Any])
async def get_template(template_id: str):
"""获取模板详情"""
template = template_manager.get_template(template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template.to_dict()
@app.post("/channels", response_model=Dict[str, Any])
async def create_channel(request: CreateChannelRequest):
"""创建渠道"""
try:
# 转换渠道类型
channel_type = ChannelType(request.type.lower())
success, result = channel_manager.create_channel(
name=request.name,
channel_type=channel_type,
config=request.config
)
if not success:
raise HTTPException(status_code=400, detail=result.get("error", "创建失败"))
return result
except ValueError:
raise HTTPException(status_code=400, detail=f"无效的渠道类型: {request.type}")
except Exception as e:
logger.error(f"创建渠道失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"创建渠道失败: {str(e)}")
@app.get("/channels", response_model=List[Dict[str, Any]])
async def list_channels(type: Optional[str] = None):
"""列出渠道"""
try:
channel_type = ChannelType(type.lower()) if type else None
channels = channel_manager.list_channels(channel_type)
return channels
except ValueError:
raise HTTPException(status_code=400, detail=f"无效的渠道类型: {type}")
except Exception as e:
logger.error(f"获取渠道列表失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取渠道列表失败: {str(e)}")
@app.post("/channels/{channel_id}/test")
async def test_channel(channel_id: str):
"""测试渠道连接"""
success, result = channel_manager.test_channel(channel_id)
if not success:
raise HTTPException(status_code=400, detail=result.get("error", "测试失败"))
return result
@app.get("/users/{user_id}/preferences", response_model=Dict[str, Any])
async def get_user_preferences(user_id: str):
"""获取用户偏好"""
preferences = user_preference_manager.get_user_preferences(user_id)
return preferences
@app.put("/users/{user_id}/preferences")
async def update_user_preferences(user_id: str, request: UpdateUserPreferencesRequest):
"""更新用户偏好"""
success = user_preference_manager.update_user_preferences(
user_id=user_id,
preferences={
"notification_types": request.notification_types,
"global_settings": request.global_settings
}
)
if not success:
raise HTTPException(status_code=500, detail="更新失败")
return {"message": "用户偏好已更新"}
@app.get("/stats", response_model=Dict[str, Any])
async def get_stats():
"""获取统计信息"""
stats = notification_service.get_stats()
return stats
@app.get("/system/status", response_model=Dict[str, Any])
async def get_system_status():
"""获取系统状态"""
stats = notification_service.get_stats()
return {
"status": "running",
"timestamp": datetime.now().isoformat(),
"queue_size": notification_service.queue.size(),
"total_sent": stats["total_sent"],
"total_failed": stats["total_failed"],
"total_retried": stats["total_retried"],
"channels": channel_manager.list_channels()
}
# 关闭事件
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭时停止通知服务"""
notification_service.stop()
5. 使用示例
python
# examples/demo.py
"""
消息通知系统使用示例
"""
import json
import time
from datetime import datetime, timedelta
from api.app import (
template_manager, channel_manager, user_preference_manager, notification_service
)
from channels.channel_adapters import ChannelType
from templates.template_manager import TemplateType
def setup_demo():
"""设置演示环境"""
print("=" * 60)
print("消息通知系统演示")
print("=" * 60)
# 创建模板
print("\n1. 创建通知模板")
print("-" * 40)
# 订单支付成功模板
order_template_content = {
"default": "尊敬的{{user.name}},您的订单{{order.order_id}}支付成功,金额¥{{order.amount}}。",
"email": """
<html>
<body>
<h2>订单支付成功</h2>
<p>尊敬的{{user.name}},您好!</p>
<p>您的订单 <strong>{{order.order_id}}</strong> 已支付成功。</p>
<p>支付金额:<strong>¥{{order.amount}}</strong></p>
<p>支付时间:{{order.payment_time}}</p>
<p>感谢您的购买!</p>
</body>
</html>
""",
"sms": "【商城通知】您的订单{{order.order_id}}支付成功,金额¥{{order.amount}}。"
}
success, result = template_manager.create_template(
name="order_payment_success",
template_type=TemplateType.EMAIL,
content=order_template_content,
description="订单支付成功通知模板",
channel="all"
)
if success:
print(f"✓ 订单支付成功模板创建成功: {result['template_id']}")
else:
print(f"✗ 模板创建失败: {result.get('error')}")
return False
# 创建邮件渠道
print("\n2. 创建邮件渠道")
print("-" * 40)
email_config = {
"smtp_server": "smtp.example.com",
"smtp_port": 587,
"username": "notification@example.com",
"password": "password123",
"from_email": "notification@example.com",
"from_name": "商城通知",
"use_tls": True
}
success, result = channel_manager.create_channel(
name="公司邮件服务器",
channel_type=ChannelType.EMAIL,
config=email_config
)
if success:
print(f"✓ 邮件渠道创建成功: {result['channel_id']}")
else:
print(f"✗ 邮件渠道创建失败: {result.get('error')}")
# 创建短信渠道
print("\n3. 创建短信渠道")
print("-" * 40)
sms_config = {
"api_key": "your_api_key",
"api_secret": "your_api_secret",
"api_url": "https://sms.example.com/api",
"signature": "商城通知"
}
success, result = channel_manager.create_channel(
name="阿里云短信",
channel_type=ChannelType.SMS,
config=sms_config
)
if success:
print(f"✓ 短信渠道创建成功: {result['channel_id']}")
else:
print(f"✗ 短信渠道创建失败: {result.get('error')}")
# 创建站内信渠道
print("\n4. 创建站内信渠道")
print("-" * 40)
in_app_config = {
"storage_backend": "database"
}
success, result = channel_manager.create_channel(
name="站内信",
channel_type=ChannelType.IN_APP,
config=in_app_config
)
if success:
print(f"✓ 站内信渠道创建成功: {result['channel_id']}")
else:
print(f"✗ 站内信渠道创建失败: {result.get('error')}")
return True
def demo_send_notification():
"""演示发送通知"""
print("\n5. 发送通知演示")
print("-" * 40)
# 模拟用户支付成功场景
context = {
"user": {
"name": "张三",
"email": "zhangsan@example.com",
"phone": "13800138000"
},
"order": {
"order_id": "ORD20231120123456",
"amount": "199.99",
"payment_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"items": [
{"name": "商品A", "price": "99.99", "quantity": 1},
{"name": "商品B", "price": "50.00", "quantity": 2}
]
}
}
# 获取可用的渠道
email_channels = channel_manager.get_channels_by_type(ChannelType.EMAIL)
sms_channels = channel_manager.get_channels_by_type(ChannelType.SMS)
in_app_channels = channel_manager.get_channels_by_type(ChannelType.IN_APP)
channels = []
if email_channels:
channels.append(email_channels[0].channel_id)
if sms_channels:
channels.append(sms_channels[0].channel_id)
if in_app_channels:
channels.append(in_app_channels[0].channel_id)
print(f"可用渠道: {channels}")
# 发送通知
success, result = notification_service.send_notification(
user_id="user_001",
template_name="order_payment_success",
context=context,
channels=channels,
priority=NotificationPriority.HIGH,
notification_type="order"
)
if success:
print(f"✓ 通知发送成功!")
print(f" 通知ID: {result['notification_id']}")
print(f" 状态: {result['status']}")
print(f" 目标渠道: {result['channels']}")
print(f" 排队时间: {result['queued_at']}")
# 等待处理完成
print("\n等待通知处理完成...")
time.sleep(2)
# 检查通知状态
status = notification_service.get_notification_status(result['notification_id'])
if status:
print(f"\n通知最终状态: {status['status']}")
print(f"发送结果: {json.dumps(status['send_results'], indent=2, ensure_ascii=False)}")
else:
print(f"✗ 通知发送失败: {result.get('error')}")
def demo_immediate_notification():
"""演示立即发送通知"""
print("\n6. 立即发送通知演示")
print("-" * 40)
# 模拟系统告警场景
context = {
"system": "订单服务",
"level": "ERROR",
"message": "数据库连接超时",
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"server": "server-01"
}
# 获取邮件渠道
email_channels = channel_manager.get_channels_by_type(ChannelType.EMAIL)
if email_channels:
success, result = notification_service.send_immediate_notification(
user_id="admin_001",
template_name="order_payment_success", # 使用现有模板,实际应该有告警模板
context=context,
channels=[email_channels[0].channel_id],
priority=NotificationPriority.URGENT
)
if success:
print(f"✓ 立即通知发送成功!")
print(f" 发送结果: {json.dumps(result, indent=2, ensure_ascii=False)}")
else:
print(f"✗ 立即通知发送失败: {result.get('error')}")
else:
print("✗ 没有可用的邮件渠道")
def demo_scheduled_notification():
"""演示计划发送通知"""
print("\n7. 计划发送通知演示")
print("-" * 40)
# 设置10秒后发送
send_time = datetime.now() + timedelta(seconds=10)
context = {
"user": {
"name": "李四"
},
"promotion": {
"title": "双十一大促",
"description": "全场商品5折起",
"start_time": "2023-11-11 00:00:00"
}
}
success, result = notification_service.schedule_notification(
user_id="user_002",
template_name="order_payment_success", # 使用现有模板
context=context,
send_time=send_time,
priority=NotificationPriority.NORMAL
)
if success:
print(f"✓ 计划通知创建成功!")
print(f" 通知ID: {result['notification_id']}")
print(f" 计划发送时间: {result['scheduled_at']}")
print(f" 状态: {result['status']}")
print(f"\n等待计划时间到达...")
time.sleep(12)
# 检查通知状态
status = notification_service.get_notification_status(result['notification_id'])
if status:
print(f"通知最终状态: {status['status']}")
else:
print(f"✗ 计划通知创建失败: {result.get('error')}")
def demo_user_preferences():
"""演示用户偏好设置"""
print("\n8. 用户偏好设置演示")
print("-" * 40)
# 获取用户当前偏好
preferences = user_preference_manager.get_user_preferences("user_001")
print(f"用户当前偏好:")
print(json.dumps(preferences, indent=2, ensure_ascii=False))
# 更新用户偏好
new_preferences = {
"notification_types": {
"order": {
"email": {"enabled": True},
"sms": {"enabled": True},
"in_app": {"enabled": True}
},
"payment": {
"email": {"enabled": True},
"sms": {"enabled": False}, # 关闭短信通知
"in_app": {"enabled": True}
},
"promotion": {
"email": {"enabled": False}, # 关闭促销邮件
"sms": {"enabled": False},
"in_app": {"enabled": True}
}
},
"global_settings": {
"quiet_hours": {"start": "23:00", "end": "08:00"},
"language": "zh-CN",
"timezone": "Asia/Shanghai"
}
}
success = user_preference_manager.update_user_preferences("user_001", new_preferences)
if success:
print(f"\n✓ 用户偏好更新成功!")
# 获取更新后的偏好
updated_preferences = user_preference_manager.get_user_preferences("user_001")
print(f"\n更新后的用户偏好:")
print(json.dumps(updated_preferences, indent=2, ensure_ascii=False))
else:
print(f"✗ 用户偏好更新失败")
def demo_system_stats():
"""演示系统统计信息"""
print("\n9. 系统统计信息")
print("-" * 40)
stats = notification_service.get_stats()
print(f"系统统计:")
print(json.dumps(stats, indent=2, ensure_ascii=False))
# 列出所有模板
templates = template_manager.list_templates()
print(f"\n模板数量: {len(templates)}")
# 列出所有渠道
channels = channel_manager.list_channels()
print(f"渠道数量: {len(channels)}")
def main():
"""主函数"""
# 设置演示环境
if not setup_demo():
print("演示环境设置失败,退出...")
return
# 运行各个演示
demo_send_notification()
demo_immediate_notification()
demo_scheduled_notification()
demo_user_preferences()
demo_system_stats()
print("\n" + "=" * 60)
print("演示完成!")
print("=" * 60)
# 显示最终状态
print(f"\n队列大小: {notification_service.queue.size()}")
print(f"总发送数量: {notification_service.stats['total_sent']}")
print(f"总失败数量: {notification_service.stats['total_failed']}")
# 停止通知服务
notification_service.stop()
if __name__ == "__main__":
main()
6. 系统部署与监控
6.1 部署架构
监控层
外部服务
数据存储层
工作节点层
消息队列层
应用层
负载均衡层
负载均衡器
API服务实例1
API服务实例2
API服务实例3
消息队列 RabbitMQ/Kafka
工作节点1
工作节点2
工作节点3
数据库 PostgreSQL
缓存 Redis
对象存储
邮件服务
短信服务
推送服务
监控系统
日志系统
告警系统
6.2 监控指标
-
系统级指标
- CPU使用率
- 内存使用率
- 磁盘使用率
- 网络I/O
-
应用级指标
- API请求率
- API响应时间
- API错误率
- 队列大小
- 处理延迟
-
业务级指标
- 通知发送成功率
- 各渠道发送统计
- 模板使用频率
- 用户活跃度
6.3 告警规则
yaml
alert_rules:
- alert: HighErrorRate
expr: rate(api_errors_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "API错误率过高"
description: "API错误率超过10%,当前值为 {{ $value }}"
- alert: QueueBacklog
expr: notification_queue_size > 1000
for: 10m
labels:
severity: warning
annotations:
summary: "通知队列积压"
description: "通知队列大小超过1000,当前值为 {{ $value }}"
- alert: ChannelFailure
expr: rate(channel_failures_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "渠道失败率过高"
description: "渠道失败率超过5%,当前值为 {{ $value }}"
7. 代码自查与优化
7.1 代码质量检查清单
-
错误处理
- 所有可能抛出异常的地方都有try-catch处理
- 错误信息明确,便于调试
- 日志记录完整
-
性能优化
- 使用缓存减少数据库查询
- 批量处理提高吞吐量
- 异步处理避免阻塞
-
可扩展性
- 抽象接口设计,便于扩展新渠道
- 配置化管理,便于调整
- 模块化设计,便于维护
-
安全性
- 输入验证和过滤
- 敏感信息加密存储
- 访问控制和认证
-
可测试性
- 依赖注入,便于单元测试
- 模拟外部服务,便于集成测试
- 配置分离,便于测试环境设置
7.2 优化建议
-
数据库优化
- 添加合适的索引
- 使用连接池
- 定期归档历史数据
-
缓存策略
- 模板内容缓存
- 用户偏好缓存
- 渠道配置缓存
-
消息队列优化
- 使用优先级队列
- 实现死信队列
- 监控队列积压
-
监控完善
- 添加链路追踪
- 实现健康检查
- 添加性能指标
-
容错机制
- 实现熔断器模式
- 添加降级策略
- 完善重试机制
8. 总结
本文详细介绍了如何设计并实现一个完整的企业级消息通知系统。我们从系统架构设计开始,逐步实现了模板管理、渠道适配、用户偏好管理、异步处理、失败重试等核心功能。
8.1 系统特点
- 多渠道支持:支持邮件、短信、推送、站内信、Webhook等多种通知渠道
- 模板化管理:使用模板引擎,支持变量替换,便于内容管理
- 用户偏好:用户可以自定义接收哪些通知,通过哪些渠道接收
- 异步处理:使用消息队列实现异步处理,提高系统吞吐量
- 可靠投递:实现失败重试机制,确保消息可靠投递
- 监控告警:完善的监控体系,及时发现和解决问题
- 易于扩展:模块化设计,便于添加新的渠道和功能
8.2 实际应用场景
- 电商系统:订单状态通知、支付成功通知、发货通知
- 社交网络:消息提醒、好友请求、点赞评论通知
- 企业应用:审批流程通知、系统告警、会议提醒
- 物联网:设备状态通知、告警信息推送
- 金融服务:交易通知、账户变动提醒、安全验证
8.3 后续改进方向
- 国际化支持:多语言模板,时区处理
- A/B测试:不同模板效果对比
- 智能路由:根据用户行为智能选择最佳渠道
- 内容分析:分析通知打开率、点击率
- 灰度发布:逐步发布新功能,降低风险
通过本文的实现,我们构建了一个功能完整、可扩展、高可用的消息通知系统。在实际应用中,还需要根据具体业务需求进行调整和优化,但本文提供的架构和实现已经为构建企业级通知系统奠定了坚实的基础。