目录
上篇文章学了celery基本使用,这篇学celery发送邮件和celery定时任务。在学celery发送邮件之前,要先会普通的发送邮件,再会celery定时任务,最后二者结合,所以celery发送邮件在本文末尾。
发送邮件
本部分中,发送邮件时未用celery。
本文中和邮件相关的所有代码,如果你那里代码没问题,但是报错,请看本部分末尾的解决报错,也可以从目录中快速查找。
发送普通邮件
配置
我将发件人的相关信息存入了系统环境变量,所以代码里是从环境变量读取。
在settings.py中添加
EMAIL_HOST=os.environ.get('MAIL_SERVER') or 'smtp.yeah.net' # 邮件服务器
# EMAIL_PORT=25 # SMTP服务的端口是25,不写也行
EMAIL_HOST_USER=os.getenv('MAIL_USERNAME') or '@yeah.net' # 用户名
EMAIL_HOST_PASSWORD=os.getenv('MAIL_PASSWORD') or '' # 密码,若设置了授权码,应使用授权码
EMAIL_FROM = 'python' + '<' + (os.getenv('MAIL_USERNAME') or '@yeah.net') + '>' # 收件人看到的发件人,要和发送邮件的邮箱相同,<前是收件人看到的发件人的昵称
发送邮件
路由的代码我就不放了,只放视图函数的。
先导入一些模块,这里的代码是本文都能用的,其中一些是后文用的,这里一起写在这。另外一些是上篇文章用的,比如waitIO。
from django.shortcuts import render
from myApp.tasks import waitIO,send_mail_via_celery
from django.http import HttpResponse
from django.core.mail import send_mail, send_mass_mail, EmailMultiAlternatives
from django.conf import settings # 导入settings.py
from django.template import loader
import os
from myApp.models import Student,Grade
from myApp.util import token_confirm
from django.urls import reverse
用的Django自带的send_mail(),所以视图函数起名时不要跟它重复。
可以一次发送一封邮件,一次发送多封邮件,还有发送渲染好的模版。这三种,注释掉任意两种,代码都能运行。
def send_mail1(request): # 普通发送邮件
'''发送一封邮件
# result=send_mail('帐号激活','请点击<a href="#">链接</a>激活你的帐号',settings.EMAIL_FROM,[os.getenv('MAIL_USERNAME')]) # 第四个参数是收件人,需放在列表中。这个环境变量是发件人,即给自己发。纯文本发送,不解析html标签,邮件源码中正文(在全部中头信息的下面)部分Content-Type: text/plain
message='请点击<a href="#">链接</a>激活你的帐号'
result=send_mail('帐号激活','',settings.EMAIL_FROM,[os.getenv('MAIL_USERNAME')],html_message=message) # 第二个参数(正文内容)可以是空字符串,html_message是要发送的html内容。以html形式发送,解析html标签,邮件源码中正文(在全部中头信息的下面)部分Content-Type: text/plain
if result==1: # yeah发送成功返回整型1,猜测返回值是发送成功的邮件数量
return HttpResponse('发送成功')
else:
return HttpResponse('发送失败')
'''
'''发送多封邮件
message1=('帐号激活1','请点击<a href="#">链接</a>激活你的帐号',settings.EMAIL_FROM,[os.getenv('MAIL_USERNAME')])
message2=('帐号激活2','请点击<a href="#">链接</a>激活你的帐号',settings.EMAIL_FROM,[os.getenv('MAIL_USERNAME')])
result=send_mass_mail((message1,message2),fail_silently=False) # 元组内是多封邮件
if result==2: # yeah发送成功返回整型2,猜测返回值是发送成功的邮件数量
return HttpResponse('发送成功')
else:
return HttpResponse('发送失败')
'''
# 发送渲染好的模版
subject='帐号激活'
from_email=settings.EMAIL_FROM
to=[os.getenv('MAIL_USERNAME')] # 收件人,需放在列表中。这个环境变量是发件人,即给自己发
html_content=loader.get_template('myApp/activate.html').render({'username':'xiaoming','activate_url':'#'}) # 渲染模版文件
message=EmailMultiAlternatives(subject=subject,from_email=from_email,to=to)
message.attach_alternative(html_content,'text/html') # 以html形式发送,解析html标签,邮件源码中正文(在全部中头信息的下面)部分Content-Type: text/html
result=message.send()
if result==1: # yeah发送成功返回整型1,猜测返回值是发送成功的邮件数量
return HttpResponse('发送成功')
else:
return HttpResponse('发送失败')
第三种的模版文件activate.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>激活</title>
</head>
<body>
<b>Hello {{ username }}:</b><br />
请点击<a href="{{ activate_url }}">链接</a>激活你的帐号
</body>
</html>
发送帐号激活邮件
业务逻辑
- 处理用户注册数据,存入数据库,并将数据库中该用户的is_activated字段设为False,且用户未激活之前不允许登录
- 产生token和激活链接,并在激活链接中使用token
- 发送激活邮件
- 用户点击邮件中的激活链接,服务端验证通过后,将该用户的is_activated字段设为True,之后用户可以登录
- 若直到激活链接过期,用户都没点击激活链接,则删除数据库中该用户的信息,并允许重新使用相同的信息进行注册,前提是这些信息在数据库中是唯一的
配置
邮箱配置和celery配置与之前相同,即settings.py中的内容,邮箱的与上面一致,celery的与上篇文章一致。
pip install itsdangerous
创建模型并迁移。Grade表中需添加一些数据,注册用户时要从这里查数据。
from django.db import models
# Create your models here.
# 复制的第十一个django项目的models.py
class Grade(models.Model):
name=models.CharField(max_length=20)
boy_number=models.IntegerField()
girl_number=models.IntegerField()
is_deleted=models.BooleanField(default=False)
def __str__(self): # 打印查到的对象时,打印该对象name属性的值;站点管理页中表中显示对象name属性的值
return self.name
class Meta:
db_table='grades'
ordering=['id']
class StudentManager(models.Manager):
def get_queryset(self):
return super(StudentManager,self).get_queryset().filter(is_deleted=False)
class Student(models.Model):
objects1=StudentManager()
name=models.CharField(max_length=20)
sex=models.BooleanField()
content=models.CharField(max_length=20)
age=models.IntegerField()
grade=models.ForeignKey('Grade',on_delete=models.CASCADE)
is_deleted=models.BooleanField(default=False)
def __str__(self):
return self.name
class Meta:
db_table='students'
ordering=['id']
封装操作token的类
在Django应用文件夹内新建util.py
from itsdangerous import URLSafeTimedSerializer as Serializer
import base64
from django.conf import settings
class Token:
def __init__(self,secret_key):
self.secret_key=secret_key
self.salt=base64.encodebytes(secret_key.encode('utf-8')) # 返回bytes对象
def generate_activate_token(self,id): # 生成用于激活帐号的token,防止激活链接中明文存放新用户的id
s=Serializer(self.secret_key)
return s.dumps(id,self.salt) # URLSafeTimedSerializer类是和时间相关的,它的对象在dumps()时会自动用上当前时间
def check_activate_token(self,token,expiration=3600): # 校验用于激活帐号的token。第三个参数是过期时间,单位秒。由于定义方法时设置了expiration的默认值,所以调用时传不传都行
s=Serializer(self.secret_key)
return s.loads(token,salt=self.salt,max_age=expiration)
def del_validate_token(self,token): # token过期或校验失败时删除token
s=Serializer(self.secret_key)
print(s.loads(token,salt=self.salt))
return s.loads(token,salt=self.salt)
token_confirm=Token(settings.SECRET_KEY) # 定义为全局变量
路由
from django.urls import path
from myApp import views
urlpatterns=[
path('register2/',views.register2), # 真实用户注册
path('activate/<token>',views.activate,name='activate'), # 激活帐号
]
视图函数
def register2(request): # 真实用户注册
if request.method=='GET':
return render(request,'myApp/register2.html')
else:
username=request.POST.get('username')
password=request.POST.get('password') # Student模型中没有password字段,所以这个字段没用
u=Student.objects1.filter(name=username).first() # 先用姓名查询,假设姓名唯一
if u:
return HttpResponse('该用户名已被使用,请更换用户名')
else:
grade=Grade.objects.get(pk=1) # 由于Student模型中的grade字段是外键,所以创建Student对象时,给grade传值时要传Grade对象,否则报错,即先从Grade模型查询,再将查到的结果传给grade,而不能直接给grade传具体的值
u=Student(name=username,sex=True,content='nothing',age=10,grade=grade)
u.save() # 存入数据库
token=token_confirm.generate_activate_token(u.id) # 虽然接收处实参写的id,但这里先将姓名传给实参(即id)
activate_url=request.get_host()+reverse('myApp1:activate',kwargs={'token':token}) # get_host()是从request.headers中返回源主机地址和端口,from https://www.cnblogs.com/MnCu8261/p/5871085.html。reverse()类似模版文件中的url标签,能根据路由的别名反向解析视图函数,冒号前是主路由文件中的namespace,冒号后是子路由文件中的name,可以用args或kwargs给视图函数传值,但它们不能同时使用
message=loader.get_template('myApp/activate.html').render({'username':u.name,'activate_url':activate_url}) # 渲染模版文件
result=send_mail('帐号激活','',settings.EMAIL_FROM,[os.getenv('MAIL_USERNAME')],html_message=message) # 第二个参数(正文内容)可以是空字符串,html_message是要发送的html内容。以html形式发送,解析html标签,邮件源码中正文(在全部中头信息的下面)部分Content-Type: text/plain
if result==1: # yeah发送成功返回整型1,猜测返回值是发送成功的邮件数量
return HttpResponse('注册成功,激活邮件已发送到你的邮箱,需点击邮件中的激活链接激活后才能使用该帐号!')
else:
return HttpResponse('激活邮件发送失败,请重新注册!')
def activate(request,token): # 激活帐号
try:
uid=token_confirm.check_activate_token(token)
except:
uid=token_confirm.del_validate_token(token)
u=Student.objects1.get(pk=uid)
u.delete() # 从数据库中删除
return HttpResponse('激活失败!请重新注册!')
try:
u=Student.objects1.get(pk=uid) # 注意objects1已过滤掉is_deleted为True的学生
except Student.DoesNotExist: # 若未找到,会触发DoesNotExist异常,捕获异常时写except 模型名.DoesNotExist as e
return HttpResponse('用户不存在,请重新注册!')
u.is_deleted=True # 激活帐号。由于Student模型中没is_activated字段,这里先用is_deleted替代
u.save()
return HttpResponse('激活成功!')
模版文件
register2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
<form action="/register2/" method="post">
{% csrf_token %}
用户名:<input type="text" name="username" /><br />
密码:<input type="password" name="password" /><br />
<input type="submit" value="注册" />
</form>
</body>
</html>
activate.html与前面发送普通邮件中的相同。
测试
启动项目,访问注册的路由,提交表单后收到邮件,执行了register2()函数;点击邮件中的链接激活帐号,执行了activate()函数。
解决报错
发送邮件时若有报错,报错里会有链接,进入链接查看邮箱服务商提供的错误代码详细介绍。另外,用网易邮箱给QQ邮箱发邮件,可能会报错554 DT:SPM,虽然介绍里说这个报错的原因是内容被识别为垃圾邮件,但内容已改为国内时政新闻,此时若你的收件人是QQ邮箱,可以尝试换成网易邮箱,只有一个网易邮箱也可以给自己发。
celery定时任务
安装flower
flower可以通过浏览器可视化地查看celery任务情况,后面有效果图。
pip install flower==0.9.7,这是写文章时py库(含文档)里的最新版,但查文档时却查到了1.0.0版英文文档,0.9.1版中文文档,这三个文档的页面还都显示版本是latest,而且后两个的页面里也没有其他版本可换。后面在启动flower时遇到了问题,又偶然查到celery文档中也有介绍。关于用法的介绍,这四篇文档的内容不太一样,如果你遇到了启动flower方面的问题,这四篇文档都可以看看。
创建定时任务
celery的配置还是上篇文章的配置,这里就不写了。
在settings.py中创建定时任务,这里的任务还用的上篇文章的waitIO
CELERYBEAT_SCHEDULE={ # celery定时任务,里面可以有多个任务,任务之间用逗号间隔,即每个任务的大括号后加逗号
'schedule-test':{
'task':'myApp.tasks.waitIO', # 值中第一项是Django应用名;第二项是celery任务文件,即tasks.py;第三项是要定时执行的celery任务名,即tasks.py中的函数名
'schedule':timedelta(seconds=15), # 每隔15秒自动执行一次celery任务
# 'schedule':crontab(minute='01',hour='15'), # 每天15点01分自动执行一次celery任务。需from celery.schedules import crontab,类unix系统通过crontab命令管理定时任务,windows不确定是否支持
'args':() # 给celery任务传的参数,放在元组里(元组内只有一项时末尾要加逗号),没参数时可以注释或写空元组
},
}
启动flower
先启动celery,再启动flower,再启动beat,后两个的顺序不能变,否则celery控制台报错,或flower控制台提示好几个失败。celery启动方式有好几种,我随便选一种,详情看上篇文章。
以下命令我是在pycharm的不同控制台执行的,如果你用的系统自带的终端,需先进入工程目录,用了虚拟环境的话要先进入虚拟环境。
启动celery
celery -A 工程名 worker -l info --pool=solo
启动flower
有两种方法
1.前面提到了四篇文档,这是第一篇文档里的部分方法
flower --port=5555 # 直接启动,默认5555端口。这种不行,启动后浏览器里看不到任务。
celery flower -A 工程名 --address=127.0.0.1 --port=5555 # 通过celery启动,可以
2.这是第四篇文档里的部分方法
celery flower [-A 工程名] --broker=redis://:redis密码@redis地址:redis端口/redis数据库序号 # 注意//后还有一个冒号
通过Broker URL启动的方法,我用的redis,文档的写法不对,要在//后加冒号(文档里没加),即用redis时--broker的值的格式和settings.py中BROKER_URL的配置一样。另外,由于第二篇文档中这种启动方法还加了-A参数,而其他文档里没这个参数,我试了加不加都行。
如果你的redis没密码,而且redis是在本地,还要用这种启动方法时,试试celery flower --broker=redis://localhost:端口/数据库序号,不行的话再在//后加冒号,我的redis是在云服务器上的,没试这种。
启动beat
celery -A 工程名 beat -l info # 启动flower后,浏览器里可能看不到任务,则需要启动beat。
beat是任务生产者,发现任务时将任务放入队列,让worker执行任务,worker是任务消费者。这句话的参考链接1,参考链接2,参考链接3,参考链接4,参考链接5。
查看celery任务情况
浏览器访问127.0.0.1:5555,默认端口是5555
这是别人的图,参考链接6

这是第四篇文档里的图


这是我的图,不知道为什么没有其他图里的Monitor标签

celery发送邮件
配置
与前面 发送帐号激活邮件 的配置相同
路由
from django.urls import path
from myApp import views
urlpatterns=[
path('send_mail2/',views.send_mail2), # celery发送邮件
]
视图函数
def send_mail2(request): # celery发送邮件
send_mail_via_celery.delay({ # 字典里的参数是send_mail()的参数
'subject':'帐号激活',
'message':'celery发送邮件测试',
'recipient_list':[os.getenv('MAIL_USERNAME')] # 这个环境变量是发件人,即给自己发
})
return HttpResponse('发送成功') # celery发送邮件成功时这里收到的返回值是django_celery_results_taskresult表中的task_id,即本次celery任务的id,不知道怎么判断是否发送成功,所以直接返回发送成功,但也有解决方法,在tasks.py中装饰器task_success.connect
创建任务(task)
在上篇文章创建的tasks.py中添加
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings # 导入settings.py
from django.http import HttpResponse
from celery.signals import task_success
@shared_task
def send_mail_via_celery(kwargs): # celery发送邮件。kwargs收到send_mail2()中传来的字典
'''
虽然平时用**kwargs时是函数声明时写**kwargs,函数内写kwargs,但这里层层调用(send_mail2()中调用send_mail_via_celery(),send_mail_via_celery()中调用send_mail())时要反过来,send_mail_via_celery()中写kwargs,函数内写**kwargs,原因
https://docs.python.org/zh-cn/3.6/library/stdtypes.html#typesmapping,python文档中搜**kwargs,结果中点内置函数,页内搜**kwarg,附近说映射类型 — dict,就是这个超链接
https://docs.python.org/zh-cn/3.6/library/stdtypes.html#mapping-types-dict,内容和上一个一样,虽然链接中的锚点不同,但指向同样的内容,进入上一个链接后,手动点击标题获得的锚点是这个
https://docs.python.org/zh-cn/3.6/glossary.html?highlight=kwargs#term-argument
'''
result=send_mail(**kwargs,from_email=settings.EMAIL_FROM) # 将是否发送成功的结果返回。**kwargs中**表示对收到的字典解析,读取里面的键值对并作为参数传递给send_mail()
if result==1: # yeah发送成功返回整型1,猜测返回值是发送成功的邮件数量
print('celery发送邮件成功')
else:
print('celery发送邮件失败')
@task_success.connect(sender=send_mail_via_celery) # 获取异步任务的执行结果。celery任务执行完成后,自动执行被装饰的回调函数,并通过信号量通知被装饰的函数,告诉它celery任务已完成,同时告诉任务结果,参数是celery任务名
def complete(sender=None,result=None,**kwargs): # result是celery任务执行结果,若celery任务没有手动return,则这里赋默认值None。必须写**kwargs,否则无法启动celery
print(result)
print('上一行是celery任务执行完成后通过信号量告诉其他函数,任务已完成,同时告诉任务结果,即手动打印了回调函数收到的celery任务的执行结果,不是celery自动打印的')
其中装饰器task_success就是上篇文章末尾提到的。
启动项目
python manage.py runserver
celery -A 工程名 worker -l info --pool=solo
访问路由,控制台打印,邮箱也收到邮件

本文介绍了如何在Django中使用celery发送邮件,包括普通邮件和激活邮件的发送,以及如何配置celery来创建定时任务。首先,详细展示了发送普通邮件的步骤,包括配置、视图函数和模板文件。接着,讲解了发送帐号激活邮件的业务逻辑,包括生成和校验token。然后,讨论了celery定时任务的设置,包括创建任务、启动flower和beat。最后,演示了如何使用celery异步发送邮件,并给出了相关代码示例。
1598

被折叠的 条评论
为什么被折叠?



