Django轻量级任务追踪管理平台开发:一

学习django怎么能没有项目练手?最近在B站捞了一个不错的实战项目,是个人比较喜欢的老师 武沛齐录制的。通过将重要的知识点记录下来,达到强化理解的作用。

一、项目展示

主页看着好炫有没有?但据说只是一张图片,哈哈哈>_<

所有状态栏都是可以点的,功能看着还挺多的。看着不错?开始干吧!

二、阶段目标与技术点

今天先完成的是最基本的注册和登录。注册时会用到短信验证码,登录会用到图片验证码。所需要的主要知识点如下:

  • 腾讯云短信发送验证码
  • redis存储验证码
  • pillow模块生成图片验证码
  • 由于不用django自带的用户表,因此会用到session和中间件middleware,以及用md5对密码进行加密
  • mysql+bootstrap
  • ajax

三、代码展示与介绍

1. 创建项目与目录结构

为了能使当前的app与之后可能的app有效分隔,templates和static都包含在app内部。系统查找templates和static会优先在项目根目录寻找,若找不到则会根据app注册的顺序,进入每个app分别查找。
创建一个tracer项目,新建一个app01,主要目录结构如下:

主要目录结构
│  manage.py
│  requirements.txt     
├─app01
│  │  admin.py
│  │  apps.py
│  │  forms.py
│  │  models.py
│  │  tests.py
│  │  urls.py
│  │  __init__.py
│  │         
│  ├─middleware
│  │  │  account.py
│  │  │  __init__.py
│  │  │  
│  │  │        
│  ├─migrations
│  │  │  __init__.py
│  │  │  
│  │  │      
│  ├─static
│  │  ├─css
│  │  ├─fonts     
│  │  ├─image
│  │  └─js
│  ├─templates
│  │  └─layout
│  │          base.html
│  │          index.html
│  │          login.html
│  │          login_sms.html
│  │          register.html
│  │          
│  ├─utils
│  │  │  common.py
│  │  │  imgcode.py
│  │  │  sms.py
│  │          
│  ├─views
│  │  │  __init__.py
│  │  │  
│  │  ├─account
│  │  │  │  views.py
│  │  │  │  __init__.py 
│  │  │          
├─tracer
│  │  asgi.py
│  │  local_settings.py
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │      
├─venv

2. 模型表

模型表很简单,只有一个用户表,目前没有写任何关联字段。

# app01/models.py
from django.db import models

class UserInfo(models.Model):
    username = models.CharField(verbose_name='用户名', max_length=32, db_index=True)
    password = models.CharField(verbose_name='密码', max_length=32)
    email = models.EmailField(verbose_name='邮箱')
    phone = models.CharField(verbose_name='手机号', max_length=16)

    def __str__(self):
        return self.username

3. 路由

虽然目前只有一个app,但为了模拟之后复杂的多app的场景,需要使用路由分发,并且为每一个app创建一个命名空间。

# tracer/urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    # 增加路由分发,命名空间
    # 注意若想添加命名空间,include后面需要跟一个元组,把app传进去
    path('app01/', include(('app01.urls', 'app01'), namespace='app01')),
]


# app01/urls.py
urlpatterns = [
    # 注册
    path('register/', views.register, name='register'),
    # 发送短信
    path('send/sms/', views.send_sms, name='send_sms'),
]

4. 注册功能实现

表模型创建好之后,就可以开始实现注册功能了。思路是用ModelForm进行数据验证和前端渲染,注册时输入手机号获取验证码,若没有问题则将数据传入视图函数中,然后直接入库。若数据校验出现问题,前端需要实时渲染,因此还需要用到Ajax.

forms中的代码
# app01/forms.py

import ...

# 定义一个类,后面的表单类如果想要bootstrap样式form-control或者一些默认的提示,可以继承这一个类
class BootstrapForm:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field_name, field in self.fields.items():
            field.widget.attrs["class"] = "form-control field"
            field.widget.attrs["placeholder"] = '请输入'+field.label
            field.error_messages['required'] = '此字段必须填写'


class RegisterForm(BootstrapForm, forms.ModelForm):
    # 需要验证的字段全部重新写一遍就好
    username = forms.CharField(label='用户名', min_length=3, max_length=6, error_messages={
        'min_length': '此字段最少3位字符', 'max_length': '此字段最多6位字符'}
    )
    # ...
    # 表模型中没有code和confrim_password,只做数据校验用
    confirm_password = forms.CharField(label='确认密码', min_length=3, widget=forms.PasswordInput(), error_messages=              
    {
        'min_length': '密码至少3位'
    })
    code = forms.CharField(label='验证码')
    

    # 下面是钩子函数,验证码若超时或错误要有错误提示,密码若没有问题应该加密返回,这样视图函数就可以直接存储了
    # ...    
    def clean_password(self):
        """加密处理"""
        password = self.cleaned_data.get("password")
        # encoder()是自己写的加密函数
        return encoder(password)
加密函数
# app01/utils/common.py

import hashlib
from django.conf import settings

def encoder(password):
    """MD5加密,settings.SECRET_KEY加盐"""
    if password:
        hash_object = hashlib.md5(settings.SECRET_KEY.encode('utf-8'))
        hash_object.update(password.encode('utf-8'))
        return hash_object.hexdigest()

前端使用bootstrap简单做个样式就行。由于表单中字段比较多,如果后期通过ajax一一获取input框中的数据会比较繁琐,因此使用form包裹一下,然后给form一个id,这样提交时只需要调用$(#regForm).serialize()就可以一次性获得所有input标签的内容了。

前端部分代码
{% for field in form %}
   <div class="form-group clearfix layout">
      <label for="{{ field.auto_id }}">{{ field.label }}</label>    
      {% if field.name == 'code' %}
      <!--验证码所在行,需要旁边添加一个按钮-->
          <div class="row">
              <div class="col-md-6">{{ field }}<span class="error-msg"></span></div>
              <div class="col-md-6">
                  <input type="button" class="btn btn-default btn-block" id="get_sms" value="获取验证码">
              </div>
          </div>
      {% else %}
          {{ field }}
          <span class="error-msg"></span>
      {% endif %}
  </div>
{% endfor %}

前端搭建好之后可以开始使用了。

第一步,点击获取验证码,直接通过腾讯云短信发送验证码到手机上。关于腾讯云短信的使用,武sir在自己的网站有介绍,有感兴趣的同学可以去看看 : https://pythonav.com/wiki/detail/10/81/

为了让项目更安全,短信的配置连同mysql都应该放在local_settings中,推送或上传时不要让别人看到!

点击按钮之后应该会出现倒计时效果,手机号会通过ajax发到后端。

发送验证码代码
let timer;
function bindSendSMSEvent(){                        
      $("#get_sms").click(function () {
      $(".error-msg").empty();
      // 未输入手机号
      if (!$("#id_phone").val()){
          alert("请输入手机号!")
          return;
      }
      // 不能重复发送验证码
      console.log(timer);
      if (timer!==undefined){
          return;
      }              
      let that = $(this);
      $.ajax({
          // 路由中使用了命名空间,因此反向解析路由是要加上app:前缀
          url: "{% url 'app01:send_sms' %}",
          type: 'get',
          dataType: 'json',
          data: {'phone': $("#id_phone").val(),'tpl':'register'},
          success: function (ret) {
              // 后台发送成功,显示倒计时
              console.log(ret);
              if (ret["status"]){
                  // 调用页面倒计时的显示
                  sendSmsReminder(that);
              }else{
                  // 显示错误信息
                  $.each(ret["msg"], function (key, value) {
                      $("#id_"+key).next().text(value);
                      $("label[for='id_"+key+"']").parent().addClass("has-error");
                  })
              }
          }
      })
  })
}
// 页面倒计时效果
function sendSmsReminder(self) {
    let time = 60;
    self.addClass("disabled");
    timer = setInterval(function () {
        time --;
        self.val(time+"秒后重新获取");
        if (time===0){
            self.removeClass("disabled").val("获取验证码");
            clearInterval(timer);
            timer = undefined;
        }
    },1000)
}

这里要注意处理一个bug,那就是若频繁点击获取验证码,就会启动多个定时器,倒计时效果就会完全乱掉。处理方法是在函数外部定义一个timer,并规定只有timerundefined才能往下走。这样当一个定时器启动之后,timer就不再是undefined了,再点击获取验证码就被直接return,不会触发定时器了。

后端将手机号和短信的模板发给form进行校验。ajax会这样发送数据:

$.ajax({
    url: "{% url 'app01:send_sms' %}",
    type: 'get',
    dataType: 'json',
    // 这里除了手机号,还要传一个tpl变量
    data: {'phone': $("#id_phone").val(),'tpl':'register'},
    success: function (ret) {...}
    })

若是注册,tpl传一个’register’,若后期短信登录,则传一个’login’。不同的单词对应不同的短信模板,毕竟注册和登录的短信模板不能相同。模板需要在腾讯云短信的界面和django代码中配置。因此tpl的值是否在我们短信的配置中,也需要验证。
但是由于tpl这个字段不在form表单中(若放在表单中,则前端也会为tpl渲染出一个input框,这显然是不合理的),因此表单中为了获取tpl就需要从request对象中拿。也就是说后端需要将request传给form表单。

# app01/views/account/views.py
# 视图函数

def send_sms(request):
    """发送短信"""
    ret = {"status": 1, "msg": ""}
    # 将request传入
    form = SendSmsForm(request, request.GET)
    if not form.is_valid():
        ret["status"] = 0
        ret["msg"] = form.errors
    return JsonResponse(ret)


# app01/forms.py

class SendSmsForm(forms.Form):
    """发送验证码时验证手机"""
    phone = forms.CharField(label='手机号', validators=[
        RegexValidator(r'^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$', '手机号格式错误'),])
    
    # 通过重写init方法获得request
    def __init__(self, request, *args, **kwargs):
        """为验证tpl传入request"""
        super(SendSmsForm, self).__init__(*args, **kwargs)
        self.request = request

接下来如果手机号没有问题,就可以生成随即验证码,然后存入redis中了。
redis使用django-redis,使用前需要先下载redis,然后执行pip install django-redis,最后在django中配置一下redis的参数就可以了。想要详细了解的话仍然可以看武sir: https://pythonav.com/wiki/detail/10/82/

# app01/forms.py

class SendSmsForm(forms.Form):
    # ...
    def clean_phone(self):        
        # 验证短信模板是否有问题
        tpl = self.request.GET.get("tpl")
        tpl_id = local_settings.SMS_TEMPLATE.get(tpl)
        if not tpl_id:
            raise ValidationError("短信模板错误")
        phone = self.cleaned_data["phone"]
        # 判断手机号是否存在
        exist = models.UserInfo.objects.filter(phone=phone).exists()
        if exist:
            raise ValidationError('手机号已存在')
        # 生成验证码
        code = random.randint(1000, 9999)
        print(code)
        sms = send_sms_single(phone, tpl_id, [code,])
        # 发送成功result是0,只要不是0就是发送失败了
        if sms["result"] != 0:
            raise ValidationError("短信发送失败,{}".format(sms['errmsg']))
        # 发送成功,将验证码写入redis
        conn = get_redis_connection()
        conn.set(phone, code, 600)
        return phone

前端将收到的验证码输入表单中,然后点击注册,再次提交表单。这里再次使用ajax提交,因为如果直接使用form提交,页面的倒计时效果会因为刷新而消失。虽然可以通过进一步的处理来解决,但是会麻烦很多。

表单提交之后再次传入form中验证,用户名、密码、邮箱的校验比较简单,主要是验证码稍微繁琐。

def clean_code(self):
    """验证码是否匹配"""
    code = self.cleaned_data.get("code")
    phone = self.cleaned_data.get("phone")
    # 建立redis连接
    conn = get_redis_connection()
    # 查询手机号是否存在,如果不存在,可能过期了,也可能根本就没有发送过
    redis_code_byte = conn.get(phone)
    if redis_code_byte:
        # redis存储的是字节类型,需要解码
        redis_code = redis_code_byte.decode("utf-8")
        if code != redis_code:
            self.add_error("code", "验证码错误")
    else:
        self.add_error("code", "验证码错误或已过期,请重新获取")
    return code

表单验证通过之后,视图函数中就可以直接入库了!使用modelform有一个好处就是不需要手动将无用的数据,比如confirm_password,code剔除,django内部已经帮我们做了。直接save()就大功告成了!

def register(request):
    ret = {"status": 1, "msg": ''}
    if request.method == 'POST':
        # 注册请求
        form = RegisterForm(request.POST)
        if form.is_valid():
            # 表单验证通过
            print(form.cleaned_data)
            form.save()
            ret["url"] = '/app01/login'
        else:
            # 表单验证失败
            ret["status"] = 0
            ret["msg"] = form.errors
        return JsonResponse(ret)
    else:
        form = RegisterForm()
    return render(request, 'layout/register.html', {'form': form})

至此,注册功能就完成了,接下来是登录的实现。

5. 登录功能实现

最常见的登录方式有两种:

  • 短信登录
  • 密码登录

1. 短信登录

以上两种我们都要实现,首先是短信登录。其实短信登录和短信注册的逻辑几乎一样,模板甚至可以直接copy注册时的。只需在forms中重写一个类,然后传到前端进行渲染,就可以显示出类似注册界面的登录界面。

唯一与注册有区别的一点在于,当手机号不存在时,应该提示报错。而注册时手机号已存在才会报错。
因此froms中应该稍微修改一下:

#app01/forms.py
class SendSmsForm(forms.Form):
    # ...
    exist = models.UserInfo.objects.filter(phone=phone).exists()
    # 注册时
    if tpl == 'register':
        if exist:
            raise ValidationError('手机号已存在')
    # 登录时
    elif tpl == 'login':
        if not exist:
            raise ValidationError('手机号不存在,请先注册')
    # 生成验证码
    code = random.randint(1000, 9999)
    # ...

验证成功之后,需要将当前用户存入session中,并添加一个超时时间。

# app01/views/account/views.py

def login_sms(request):
    """短信登录"""
    ret = {"status": 1, "msg": ''}
    if request.method == 'POST':
        form = LoginSmsForm(request.POST)
        if form.is_valid():
            # 将用户信息写入session,登录成功之后使用
            user = form.cleaned_data.get('phone')
            request.session['user_id'] = user.id
            request.session['user_name'] = user.username
            # session过期时间要重新设置
            request.session.set_expiry(60*3600*24*7)
            ret['url'] = '/app01/index'
        else:
            ret['status'] = 0
            ret['msg'] = form.errors
        return JsonResponse(ret)
    else:
        form = LoginSmsForm()
    return render(request, "layout/login_sms.html", {"form": form})

这样,手机号登录就完成了。

2. 密码登录

密码登录需要输入的是用户名、密码和验证码,界面如图所示:

密码登录最复杂的就是生成图片验证码。这里使用pillow模块。关于pillow如何生成图片,继续参考武sir的博客:https://www.cnblogs.com/wupeiqi/articles/5812291.html
验证码的思路是:给前端的img标签一个url地址,这样它就会再次向服务器发请求,这时我们调用写好的生成验证码的函数将图片生成,通过BytesIO写入内存中,最后再从内存中读取这个图片,返还给前端。

<!--前端img标签-->
<img src="/app01/get/img/" id="img_code">
# 后端调用生成验证码的函数,将验证码返回

def get_img(request):
    """生成验证码"""
    from app01.utils.imgcode import check_code
    # check_code()就是生成验证码的函数
    img, code = check_code()
    print(code)
    # 验证码写入session
    request.session['code'] = code
    # 设置图片验证码超时时间为2分钟
    request.session.set_expiry(120)
    # 将图片写入内存
    from io import BytesIO
    io_obj = BytesIO()
    img.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())

最后登录时进行校验即可。这里我们的用户名使用邮箱或手机号,也就是邮箱号+密码或手机号+密码都可以。需要用到Q查询。

# app01/views/account/views.py

def login(request):
    # ...
    user_obj = models.UserInfo.objects.filter(Q(email=username) | Q(phone=username)).filter(
                password=password).first()
    if not user_obj:
                form.add_error("username", "用户名或密码错误")
    # ...

这些问题解决了之后,就可以完成密码登录了。

6. 登录检测显示

若用户未登录,导航条应该显示“登录”和“注册”,若用户已经登录,导航条应该显示用户名和更多操作:
用户未登录

用户已登录
为了判断用户的登录状态,需要用到中间件middleware。在请求来之前判断session中是否存在数据,若存在则表示用户已登录,于是将此用户对象传入request

# app01/middleware/account.py

class LoginMiddleware(MiddlewareMixin):
    def process_request(self, request):
        user_id = request.session.get("user_id")
        request.tracer = UserInfo.objects.filter(id=user_id).first()
        print(request.tracer)

前端做一个判断即可:

{% if request.tracer %}
<li><a href="{% url 'app01:logout' %}">登出</a></li>
<li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ request.tracer.username }}<span class="caret"></span></a>
    <ul class="dropdown-menu">
       <li><a href="#">用户资料</a></li>
       <li role="separator" class="divider"></li>
       <li><a href="#">后台管理</a></li>
    </ul>
</li>
{% else %}
<li><a href="{% url 'app01:register' %}">注册</a></li>
<li><a href="{% url 'app01:login' %}">登录</a></li>
{% endif %}

至此,登录功能也全部实现!

四、总结

今天主要完成了注册和登录功能,虽然逻辑比较简单但也还是有一些重要的知识点的。之后我会跟着老师继续前进,进行第二部分:后台管理的一些列操作。各位若有什么意见或建议,欢迎留言哦。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值