第一篇:BBS表设计及注册登录功能实现
第一篇:BBS表设计及注册登录功能实现
bbs是一个前后端不分离的全栈项目,前端和后端都需要我们自己一步一步完成。
一、表创建及同步
1、表设计
一个项目中最重要的不是业务逻辑的书写,而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺。
我们分析博客园,了解到我们需要建立7张表。
"""1.用户表"""
使用userInfo替代原有的auth_user表,继承AbstractUser
# 拓展:
phone 电话号码
avatar 用户头像
create_time 创建时间
# 外键字段
一对一个人站点表
"""2.个人站点表"""
# 普通字段
site_name 站点名称
site_title 站点标题
site_theme 站点样式
"""3.文章分类表"""
# 普通字段
name 标签名
# 外键字段
一对多个人站点
"""4.文章标签表"""
# 普通字段
name 标签名
# 外键字段
一对多个人站点
"""5.文章表"""
# 普通字段
title 文章标题
desc 文章简介
content 文章内容
create_time 发布时间
# 数据库字段设计优化(******)(虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效低)
up_num 点赞数
down_num 点踩数
comment_num 评论数
# 外键字段
一对多个人站点
多对多文章标签
一对多文章分类
"""6.点赞点踩表"""
记录哪个用户给哪篇文章点了赞还是点了踩
# 外键字段
user ForeignKey(to="User")
article ForeignKey(to="Article")
is_up BooleanField()
"""
eg: 1 1 1
1 2 1
1 3 0
2 1 1
"""
"""7.文章评论表"""
记录哪个用户给哪篇文章写了哪些评论内容
user ForeignKey(to="User")
article ForeignKey(to="Article")
content CharField()
comment_time DateField()
# 自关联
# parent ForeignKey(to="Comment",null=True)
# ORM专门提供的自关联写法
parent ForeignKey(to="self",null=True)
"""
eg: id use_id article_id parent
1 1 1
2 2 1 1
"""
以图片的形式进行表示。
2、表创建
由于django自带的sqlite数据库对日期不敏感,所以我们使用mysql数据库。
"""models.py"""
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
# 1.用户表
class UserInfo(AbstractUser):
# 手机号
phone = models.BigIntegerField(verbose_name='手机号', null=True)
# 头像
"""给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段只保存文件路径avatar/default.png"""
avatar = models.FileField(verbose_name='头像', upload_to='avatar/', default='avatar/default.png')
# 创建时间
create_time = models.DateField(verbose_name='创建时间', auto_now_add=True)
# 一对一个人站点表
blog = models.OneToOneField(to='Blog', null=True)
# 2.个人站点表
class Blog(models.Model):
# 站点名称
site_name = models.CharField(verbose_name='站点名称', max_length=32)
# 站点标题
site_title = models.CharField(verbose_name='站点标题', max_length=32)
# 站点样式【存css/js的文件路径】
site_theme = models.CharField(verbose_name='站点样式', max_length=64)
# 3.文章分类表
class Category(models.Model):
# 分类名
name = models.CharField(verbose_name='分类名', max_length=32)
"""一对多个人站点"""
blog = models.ForeignKey(to='Blog', null=True)
# 4.文章分类表
class Tag(models.Model):
# 标签名
tag = models.CharField(verbose_name='标签名', max_length=32)
"""一对多个人站点"""
blog = models.ForeignKey(to='Blog', null=True)
# 5.文章表
class Article(models.Model):
# 文章标题
title = models.CharField(verbose_name='文章标题',max_length=64)
# 文章简介
desc = models.CharField(verbose_name='文章简介',max_length=255)
# 文章内容 有很多文章内容,一般情况下都是使用TextField
content = models.TextField(verbose_name='文章内容')
# 创建时间
create_time = models.DateField(verbose_name='创建时间', auto_now_add=True)
"""数据库字段设计优化"""
# 点赞数
up_num = models.BigIntegerField(verbose_name='点赞数', default=0)
# 点踩数
down_num = models.BigIntegerField(verbose_name='点踩数', default=0)
# 评论数
comment_num = models.BigIntegerField(verbose_name='评论数', default=0)
"""外键字段"""
# 一对多个人站点
blog = models.ForeignKey(to='Blog', null=True)
# 一对多文章分类
category = models.ForeignKey(to='Category',null=True)
# 多对多文章标签
tags = models.ManyToManyField(
to='Tag',
through='Article2Tag',
through_fields=('article', 'tag')
)
# 半自动多对多表【自己写第三张表,方便拓展】
class Article2Tag(models.Model):
article = models.ForeignKey(to='Article')
tag = models.ForeignKey(to='Tag')
# 6.点赞点踩表
class UpAndDown(models.Model):
# 哪个用户
user = models.ForeignKey(verbose_name='哪个用户', to='UserInfo')
# 那篇文章
article = models.ForeignKey(verbose_name='那篇文章', to='Article')
# 是否点赞
is_up = models.BooleanField(verbose_name='是否点赞') # 传布尔值 存0/1
# 7.文章评论表
class Comment(models.Model):
# 那个用户
user = models.ForeignKey(verbose_name='哪个用户', to='UserInfo')
# 那篇文章
article = models.ForeignKey(verbose_name='哪篇文章', to='Article')
# 评论内容
content = models.CharField(verbose_name='评论内容', max_length=255)
# 评论时间
comment_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
# 自关联
parent = models.ForeignKey(to='self', null=True) # 有些评论本身就是根评论
创建结果如下所示。
二、注册功能
注册用户需要用到forms组件,我们之前是直接在views.py中书写的forms组件代码,但是为了接耦合,应该将所有的forms组件代码单独写到一个地方。
如果项目至始至终只用到一个forms组件,那么你可以直接建一个py文件书写即可,即myform.py
, 但是如果项目需要使用多个forms组件,那么你可以创建一个文件夹,在文件夹内根据forms组件功能的不同,来创建不同的py文件。
myforms文件夹
-- regform.py
-- loginform.py
-- userform.py
-- orderform.py
...
在这里我们只在应用下创建一个myforms.py
文件,方便书写。
注册功能代码如下。
- myforms.py
# 书写针对用户表的forms组件代码
from django import forms
from app01 import models
class MyRegForm(forms.Form):
# 用户名
username = forms.CharField(
label='用户名',
min_length=3,
max_length=8,
error_messages={
'required': '用户名不能为空',
'min_length': "用户名最少3位",
'max_length': "用户名最大8位"
},
widget=forms.widgets.TextInput(
# 需要让标签有bootstrap样式
attrs={'class': 'form-control'}
),
)
# 用户密码
password = forms.CharField(
label='密码',
min_length=3,
max_length=8,
error_messages={
'required': '密码不能为空',
'min_length': "密码最少3位",
'max_length': "密码最大8位"
},
widget=forms.widgets.PasswordInput(
# 需要让标签有bootstrap样式
attrs={'class': 'form-control'}
)
)
# 确认密码
confirm_password = forms.CharField(
label='确认密码',
min_length=3,
max_length=8,
error_messages={
'required': '确认密码不能为空',
'min_length': "确认密码最少3位",
'max_length': "确认密码最大8位"
},
widget=forms.widgets.PasswordInput(
# 需要让标签有bootstrap样式
attrs={'class': 'form-control'}
)
)
# 邮箱
email = forms.CharField(
label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确'
},
widget=forms.widgets.EmailInput(
# 需要让标签有bootstrap样式
attrs={'class': 'form-control'}
)
)
# 局部钩子:校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
# 去数据库中校验
is_exist = models.UserInfo.objects.filter(username=username).first()
if is_exist:
# 提示信息
self.add_error('username', '用户名已存在')
return username
# 全局钩子:校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
- register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<!--这里我们不用form表单提交数据,使用ajax,只是单纯的用一下form标签而已-->
<h1 class="text-center">注册功能</h1>
<form id="myform">
<!--csrf校验[即便下面ajax也会传递随机字符串,这里也不能取消]-->
{% csrf_token %}
<!--form组件input框-->
{% for form in form_obj %}
<div class="form-group">
<!--form.auto_id能够直接拿到的form里面input框的id值-->
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }}
<!-- ajax提交form表单,span标签里面书写没用-->
<span style="color: red;" class="pull-right"></span>
</div>
{% endfor %}
<!--头像相关-->
<div class="form-group">
<!--点击头像,直接输入文件-->
<label for="myfile">头像
{% load static %}
<img src="{% static 'img/default.png' %}" id="myimg" alt="" width="100px" style="margin-left: 10px">
</label>
<!--隐藏输入文件框-->
<input type="file" name="avatar" style="display: none" id="myfile">
</div>
<!--input提交按钮,不适用submit,使用ajax-->
<input type="button" class="btn btn-primary pull-right" value="注册" id="id_commit">
</form>
</div>
</div>
</div>
<script>
// 显示头像
$("#myfile").change(function () {
// 文件阅读器对象
// 1 先生成一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3 将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj) // 异步操作 IO操作
// 4 利用文件阅读器将文件展示到前端页面 修改src属性
// 等待文件阅读器加载完毕之后再执行
myFileReaderObj.onload = function(){
$('#myimg').attr('src',myFileReaderObj.result)
}
})
// ajax提交form表单,并返回相关信息
$('#id_commit').click(function () {
// 发送ajax请求 我们发送的数据中即包含普通的键值也包含文件
let formDataObj = new FormData();
// 1.添加普通的键值对
{#console.log($('#myform').serializeArray()) // [{name: "username", value: "yangyi"},{{name: "password", value: "123"}}, ...] 只包含普通键值对#}
/*
* [{name: "csrfmiddlewaretoken", value: "4ISNTAFRHyjx3KUPVZmeLzc4BqKq5hFqdcTNLRhrsGnfmX9NsaMTIAh1pvrfSVU8"},
* {name: "username", value: "yangyi"},
* {name: "password", value: "123"},
* ...
* ]
* */
$.each($('#myform').serializeArray(),function (index,obj) {
{#console.log(index,obj)#} // obj = {}
formDataObj.append(obj.name,obj.value)
});
// 2.添加文件数据
formDataObj.append('avatar',$('#myfile')[0].files[0]);
// 3.发送ajax请求
$.ajax({
url:"",
type:'post',
data:formDataObj,
// 需要指定两个关键性的参数
contentType:false,
processData:false,
success:function (args) {
// args为ajax返回的对象,已经反序列化,为object
if (args.code==1000){
// 跳转到登陆页面
window.location.href = args.url
}else{
// 如何将对应的错误提示展示到对应的input框下面
// forms组件渲染的标签的id值都是 id_字段名
$.each(args.msg,function (index,obj) {
{#console.log(index,obj) // username ["用户名不能为空"]#}
let targetId = '#id_' + index;
// jQuery的链式操作【$(targetId).next()拿到span标签的jQuery格式】【.parent() 拿到input】
$(targetId).next().text(obj[0]).parent().addClass('has-error')
})
}
}
})
})
// 给所有的input框绑定获取焦点事件
$('input').focus(function () {
// 将input下面的span标签和input外面的div标签修改内容及属性
$(this).next().text('').parent().removeClass('has-error')
})
</script>
</body>
</html>
- views.py
from django.shortcuts import render, HttpResponse
from app01.myforms import MyRegForm # 模块必须全名
from app01 import models
from django.http import JsonResponse
# Create your views here.
# 注册功能
def register(request):
form_obj = MyRegForm()
if request.method == 'POST':
# print(request.POST) # <QueryDict: {'csrfmiddlewaretoken': ['QAjwUsyzwKZzcT61ECviILzDQQXX3PnWZ4kwMJa9hS3hv6lZbNVXFMEAEVEMQtCE'], 'username': ['yangyi'], 'password': ['123'], 'confirm_password': ['123'], 'email': ['123@qq.com']}>
# ajax返回的字典
back_dic = {"code": 1000, 'msg': ''}
# 校验数据是否合法
form_obj = MyRegForm(request.POST)
# 判断数据是否合法
if form_obj.is_valid():
print(form_obj.cleaned_data) # {'username': 'yangyi', 'password': '123', 'confirm_password': '123', 'email': '123@qq.com'}
clean_data = form_obj.cleaned_data # 将校验通过的数据字典赋值给一个变量
# 将字典里面的confirm_password键值对删除
clean_data.pop('confirm_password') # {'username': 'yangyi', 'password': '123', 'email': '123@qq.com'}
# 用户头像
file_obj = request.FILES.get('avatar')
"""
针对用户头像一定要判断是否传值,不能直接添加到字典里面去
因为如果不传值,会将本来的default的图像变为none
"""
if file_obj:
clean_data['avatar'] = file_obj
# 直接操作数据库保存数据
models.UserInfo.objects.create_user(**clean_data)
back_dic['url'] = '/login/'
# 如果数据不合法
else:
back_dic['code'] = 2000
back_dic['msg'] = form_obj.errors # form_obj.errors 字典套列表形式,类似{'username': ['用户名不能为空']}
# ajax返回字典
return JsonResponse(back_dic)
return render(request,'register.html',locals())
# 登录功能
def login(request):
return HttpResponse('登陆界面')
最终效果如下所示。
数据库中数据为
文件目录结构为
三、登录功能
1、验证码测试
测试验证码的前端代码【关于验证码字体,我们可以访问字体网站】
"""login.html"""
<div class="col-md-6">
<!--每次刷新页面的时候,向/get_code/路径发送get请求-->
<!--img标签的src属性:1.图片路径 2.url 3.图片的二进制数据-->
<img src="/get_code/" alt="" width="430" height="35" id="id_img">
</div>
<script>
// 随机生成验证码
$('#id_img').click(function () {
// 生成一个路径,加?即可
let new_url = $(this).attr('src') + '?'
$(this).attr('src', new_url)
})
</script>
测试验证码的后端代码
"""第一种方式 直接获取后端现成的图片二进制数据发送给前端"""
def get_code(request):
with open('static/img/美女.jpg', mode='rb') as f:
data = f.read()
return HttpResponse(data)
"""第二种方式 利用pillow模块动态产生图片"""
"""
使用pillow模块:图片相关模块
-- Image:生成图片
-- ImageDraw:能够在图片上乱涂乱画
-- ImageFont:控制字体样式
"""
# 随机生成三基色
def get_random():
import random
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
from PIL import Image, ImageDraw, ImageFont # pillow模块导入方式比较特殊
def get_code(request):
# 第二种方式 利用pillow模块动态产生图片
# img_obj = Image.new('RGB', (430, 35), 'green')
img_obj = Image.new('RGB', (430, 35), get_random())
# 将图片对象保存
with open('static/img/xxx.png', 'wb') as f:
img_obj.save(f, 'png')
with open('static/img/xxx.png', 'rb') as f:
data = f.read()
return HttpResponse(data)
"""第三种方式 文件存储繁琐IO操作效率低,借助于内存管理器模块"""
"""
内存管理器模块
BytesIO:临时帮你存储数据,返回的时候数据是二进制
StringIO:临时帮你存储数据,返回的时候数据是字符串
"""
def get_code(request):
# 生成一个图片文件
img_obj = Image.new('RGB', (430, 35), get_random())
# 生成一个内存管理器对象,你可以看成是文件句柄
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
"""第四种方式:写图片验证码"""
def get_code(request):
# 生成图片对象
img_obj = Image.new('RGB', (430, 35), get_random())
# 在图片上生成画笔对象
img_draw = ImageDraw.Draw(img_obj)
# 图片字体设置 【我们的计算机上面致所有能够输出各式各样的字体样式,内部其实对应的是.ttf或.otf结尾的文件】
img_font = ImageFont.truetype('static/font/222.ttf', 30)
# 生成随机验证码【5位】
code = ''
for i in range(5):
# 大写字符
random_upper = chr(random.randint(65, 90))
# 小写字符
random_lower = chr(random.randint(97, 122))
# 随机数字
random_int = str(random.randint(0, 9))
# 从上面三个里面随机选择一个
tmp = random.choice([random_upper, random_lower, random_int])
"""
为什么一个个写而不是生成好了之后再写?
因为一个个写能够控制每个字体的间隙,而生成好之后再写的话,间隙就没法控制了
"""
img_draw.text((i*60+60, -2), tmp, get_random(), img_font)
# 拼接随机字符串【用来验证】
code += tmp
print(code)
# 随机验证码在登陆的视图函数里面需要用到,要比对,所以要找地方存起来并且其他视图函数也能拿到
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
好了,到了现在,我们可以实现图片验证码功能,效果如下。
2、完整登录代码
- login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1 class="text-center">登陆界面</h1>
<!--不用form组件,使用ajax提交数据-->
<!--用户名-->
<div class="form-group">
<label for="username">用户名</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<!--用户密码-->
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<!--验证码-->
<div class="form-group">
<div class="row">
<!--输入框-->
<div class="col-md-6">
<label for="id_code">验证码</label>
<input type="text" name="code" id="id_code" class="form-control">
</div>
<!--图片验证码-->
<div class="col-md-6" style="padding-top: 25px;">
<!--每次刷新页面的时候,向/get_code/路径发送get请求-->
<!--img标签的src属性:1.图片路径 2.url 3.图片的二进制数据-->
<img src="/get_code/" alt="" width="430" height="35" id="id_img">
</div>
</div>
</div>
<!--登录按钮-->
<input type="button" class="btn btn-success" value="登陆" id="id_commit">
<span style="color:red;" id="id_span"></span>
</div>
</div>
</div>
<script>
// 随机生成验证码
$('#id_img').click(function () {
// 生成一个路径,加?即可
let new_url = $(this).attr('src') + '?'
$(this).attr('src', new_url)
})
// button按钮提交数据请求
$('#id_commit').click(function () {
// ajax请求
$.ajax({
url: '/login/', // 提交给login函数
type: 'post',
data:{
'username': $('#username').val(),
'password': $('#password').val(),
'code': $('#id_code').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}' // csrf校验,注意加 ''
},
success:function (args) {
if(args.code === 1000){
// 跳转到如果登陆成功,跳转到home页面
window.location.href = args.url
}else{
$('#id_span').text(args.msg)
}
}
})
})
</script>
</body>
</html>
- views.py
from django.shortcuts import render, HttpResponse, redirect
from app01 import models
from django.contrib import auth
from django.http import JsonResponse
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO, StringIO
# 登录功能
def login(request):
# 如果是ajax请求
if request.is_ajax():
if request.method == 'POST':
# 返回给ajax的字典
back_dic = {'code': 1000, 'msg': ''}
# print(request.POST) # <QueryDict: {'username': ['leichao'], 'password': ['123'], 'code': ['mN4ms'], 'csrfmiddlewaretoken': ['wD2DsF3rmZKgTTPfJWdC9tay3pT9qUUSF73DkWF177OYc64dg7Dh6ufvRuAYdy9A']}>
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 比对验证码是否正确
if code == request.session.get('code'):
# 比对用户名密码是否正确【auth模块】【有则返回用户对象,没有返回None】
user_obj = auth.authenticate(username=username, password=password)
# 判断用户是否存在
if user_obj:
# 登录用户【即用户信息保存到session中】
auth.login(request, user_obj)
# 补充字典信息【返回一个url】
back_dic['url'] = '/home/'
else:
back_dic['code'] = 2000
back_dic['msg'] = '用户名或密码不正确'
else:
back_dic['code'] = 3000
back_dic['msg'] = '验证码不正确'
return JsonResponse(back_dic)
return render(request, 'login.html')
# 随机生成三基色
def get_random():
import random
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
# 生成验证码
def get_code(request):
# 生成图片对象
img_obj = Image.new('RGB', (430, 35), get_random())
# 在图片上生成画笔对象
img_draw = ImageDraw.Draw(img_obj)
# 图片字体设置
img_font = ImageFont.truetype('static/font/111.otf', 30)
# 生成随机验证码【5位】
code = ''
for i in range(5):
# 大写字符
random_upper = chr(random.randint(65, 90))
# 小写字符
random_lower = chr(random.randint(97, 122))
# 随机数字
random_int = str(random.randint(0, 9))
# 从上面三个里面随机选择一个
tmp = random.choice([random_upper, random_lower, random_int])
"""
为什么一个个写而不是生成好了之后再写?
因为一个个写能够控制每个字体的间隙,而生成好之后再写的话,间隙就没法控制了
"""
img_draw.text((i * 60 + 60, -2), tmp, get_random(), img_font)
# 拼接随机字符串【用来验证】
code += tmp
print(code)
# 随机验证码在登陆的视图函数里面需要用到,要比对,所以要找地方存起来并且其他视图函数也能拿到
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人