celery发送邮件和celery定时任务

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

目录

发送邮件

发送普通邮件

配置

发送邮件

发送帐号激活邮件

业务逻辑

配置

封装操作token的类

路由

视图函数

模版文件

测试

解决报错

celery定时任务

安装flower

创建定时任务

启动flower

启动celery

启动flower

启动beat

查看celery任务情况

celery发送邮件 

配置

路由

视图函数

 创建任务(task)

启动项目


上篇文章学了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>

发送帐号激活邮件

业务逻辑

  1. 处理用户注册数据,存入数据库,并将数据库中该用户的is_activated字段设为False,且用户未激活之前不允许登录
  2. 产生token和激活链接,并在激活链接中使用token
  3. 发送激活邮件
  4. 用户点击邮件中的激活链接,服务端验证通过后,将该用户的is_activated字段设为True,之后用户可以登录
  5. 若直到激活链接过期,用户都没点击激活链接,则删除数据库中该用户的信息,并允许重新使用相同的信息进行注册,前提是这些信息在数据库中是唯一的

配置

邮箱配置和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

访问路由,控制台打印,邮箱也收到邮件

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值