学习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
.
# 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
,并规定只有timer
是undefined
才能往下走。这样当一个定时器启动之后,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 %}
至此,登录功能也全部实现!
四、总结
今天主要完成了注册和登录功能,虽然逻辑比较简单但也还是有一些重要的知识点的。之后我会跟着老师继续前进,进行第二部分:后台管理的一些列操作。各位若有什么意见或建议,欢迎留言哦。