Django DRF根据选中userid获取用户邮箱实现告警邮件定向发送
DRF 价格告警定向发送邮件实现方案
核心问题定位
现有代码存在4个直接导致功能异常的问题:
- 模型字段冗余:
SectionAlerts表同时设计了字符串类型user、关联用户的外键userid、关联用户的外键email三个和用户相关的字段,数据重复存储且赋值逻辑混乱 - 创建告警时用户关联不完整:
perform_create方法仅给字符串类型的user字段传了当前用户对象,userid、email两个外键字段始终为空 - 发信逻辑完全错误:触发告警条件后遍历全量用户表发邮件,没有读取当前告警条目的关联用户作为收件人,导致所有用户都会收到邮件
- 业务逻辑位置错误:发邮件、删除告警这类有数据修改副作用的逻辑写在了
get_queryset方法中,只要访问告警列表接口就会触发发信,不符合HTTP接口规范,容易导致重复发信
分步修正方案
1. 调整模型结构(models.py)
删除冗余的用户关联字段,仅保留一个用户外键,用户邮箱、用户名等信息直接通过外键关联查询,不需要重复存储:
from django.db import models from ticker.models import Ticker from users.models import NewUser class SectionAlerts(models.Model): id = models.AutoField(primary_key=True) valuenow = models.FloatField(null=True, blank=True, default=None) valuealarm = models.FloatField(null=True, blank=True, default=None) # 单外键关联用户,调整related_name为语义明确的值 user = models.ForeignKey( NewUser, related_name='price_alerts', on_delete=models.CASCADE ) ticker = models.ForeignKey(Ticker, on_delete=models.CASCADE)
注意:修改完模型后需要依次执行
python manage.py makemigrations、python manage.py migrate同步数据库结构,测试阶段可以先清空原有告警表数据避免迁移冲突。
2. 修正序列化器逻辑(serializers.py)
移除冗余的用户邮箱嵌套序列化,直接通过用户外键读取邮箱字段,同时设置用户字段为只读,由后端自动赋值,不接收前端传参:
from rest_framework import serializers from ticker.models import Ticker from alerts.models import SectionAlerts from ticker.serializers import TickerSerializer class AlertsSerializer(serializers.ModelSerializer): ticker = TickerSerializer(read_only=True) # 直接通过关联的user对象取邮箱,设为只读 email = serializers.EmailField(source='user.email', read_only=True) ticker_id = serializers.SlugRelatedField( queryset=Ticker.objects.all(), source="ticker", slug_field='crypto', write_only=True ) class Meta: model = SectionAlerts fields = ['id', 'valuenow', 'valuealarm', 'ticker', 'ticker_id', 'user', 'email'] read_only_fields = ['user']
3. 修正视图层逻辑(views.py)
首先给创建、查询告警的接口加上登录权限校验,创建告警时正确绑定当前登录用户到外键;其次把发邮件、删告警的逻辑从列表查询接口中移出,修正发信时的收件人读取逻辑:
3.1 修正创建告警接口
from rest_framework import generics from rest_framework.permissions import IsAuthenticated from .serializers import AlertsSerializer from .models import SectionAlerts class CreateSectionAlerts(generics.CreateAPIView): serializer_class = AlertsSerializer # 必须加登录权限,否则匿名请求无法获取request.user permission_classes = [IsAuthenticated] def perform_create(self, serializer): # 保存时直接绑定当前登录用户,邮箱后续通过user.email读取即可 serializer.save(user=self.request.user)
3.2 修正列表查询接口
移除接口内的发信、删数据逻辑,列表接口仅负责返回告警数据:
class ListSectionAlerts(generics.ListAPIView): serializer_class = AlertsSerializer permission_classes = [IsAuthenticated] def get_queryset(self): # 支持通过url传pk查指定用户告警,默认返回当前登录用户自己的告警 if 'pk' in self.kwargs: return SectionAlerts.objects.filter(user_id=self.kwargs["pk"]) return SectionAlerts.objects.filter(user=self.request.user)
3.3 修正告警检查与发信逻辑
不要把这类逻辑放在GET请求流程中,建议放到Celery定时任务、Django自定义命令里周期性执行,核心发信逻辑如下,禁止遍历全量用户:
from django.db.models import F from .utils import Util def check_triggered_alerts(): # 一次性查出所有满足触发条件的告警,select_related提前连表查用户和币种数据,减少DB查询 triggered_alerts = SectionAlerts.objects.select_related('user', 'ticker').filter( ticker__c_0__lt=F('valuealarm') ) for alert in triggered_alerts: # 收件人直接取当前告警绑定的用户邮箱 email_body = ( f'Hi {alert.user.user_name}, you received this message because the value of the crypto ' f'{alert.ticker} is now {alert.ticker.c_0}€ and reached the alert value of {alert.valuealarm}€.\n' f'This email confirms this event.\n' f'Your Cryptothlon Team' ) data = { 'email_body': email_body, 'to_email': alert.user.email, 'email_subject': 'Crypto alert' } Util.send_email(data) # 发信完成后删除该条告警 alert.delete()
额外优化建议
- 删除、修改告警的接口也要加权限校验,确保用户只能操作自己创建的告警,避免越权
- 发邮件逻辑加异常捕获,避免某封邮件发送失败导致整个检查流程中断
- 可以给告警加
is_sent标记字段代替直接删除,方便后续排查发信记录
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

