BBS项目流程 表分析 注册功能
项目开发基本流程
这里不展开叙述了,不懂就回头看博客,ATM项目开发:https://www.cnblogs.com/zhihuanzzh/p/16841385.html
1.需求分析
2.架构设计
3.分组开发
4.提交测试
5.交付上线
项目流程
仿造博客园项目(bbs项目)分析
- 使用的技术:Django(2.2.22)、MySQL(5.6)、python3.8、jquery3.6.1、bootstrap3.4.1
- 实现的功能
注册 (forms校验,页面渲染,上传头像)
登录 (自定义图片验证码)
首页:文章展示、侧边栏过滤(分类,标签,时间)
文章详情:点赞点踩、评论(父评论和子评论)
后台管理:当前用户文章展示(文章增删改查)
发布文章
核心:文章的增删改查
表分析
先确定表的数量 再确定表的基础字段 最后确定表的外键字段
表数量分析
1.用户表
2.个人站点表
3.文章表
4.文章分类表
5.文章标签表
6.点赞点踩表
7.文章评论表
基础字段分析
ps:下列表字段设计仅供参考,我们可以有更多的想法(自行添加字段)
- 用户表
1.替换auth_user表并扩展额外的字段
2.添加电话号码、头像、注册时间三个字段
- 个人站点表
1.站点名称(jason\lili\kevin)
2.站点标题(努力奋斗去他妹的)
3.站点样式(css文件)
- 文章表
1.文章标题
2.文章简介
3.文章内容
4.发布时间
- 文章分类表
1.分类名称
- 文章标签表
1.标签名称
- 点赞点踩表
记录哪个用户给哪篇文章点了推荐(赞)还是反对(踩)
1.用户字段(用户主键)>>>:外键字段
2.文章字段(文章主键)>>>:外键字段
3.点赞点踩
- 文章评论表
记录哪个用户给哪篇文章评论了什么内容
1.用户字段(用户主键)>>>:外键字段
2.文章字段(文章主键)>>>:外键字段
3.评论内容
4.评论时间
5.外键字段(自关联,用于实现针对评论的回复功能)
id user_id article_id content parent_id
1 1 1 哈哈哈 null
2 2 1 哈你妹 1
3 3 1 讲文明 2
外键字段
-
用户表
用户与个人站点是一对一外键关系 -
个人站点表
因为跟用户表是一对一的外键关系,我们可以看成两张表是一张表 -
文章表
1.文章表与个人站点表是一对多外键关系
2.文章表与文章分类表是一对多外键关系
3.文章表与文章标签表是多对多外键关系
数据库字段优化设计:
我们想统计文章的评论数、点赞数
通过文章数据跨表查询到文章评论表中对应的数据统计即可
但是文章需要频繁的展示,如果每次都跨表查询的话效率极低
因此我们想到在文章表中再创建三个普通字段
之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三个普通字段即可(即进行加减操作进行统计
这里是三个普通字段
4.文章评论数
5.文章点赞数
6.文章点踩数
-
文章分类表
文章分类与个人站点是一对多外键关系 -
文章标题表
文章标签与个人站点是一对多外键关系
前期准备
-
步骤一:
使用pycharm创建Django项目,并且顺带让他自动创建一个应用(app01) -
步骤二:
使用MySQL创建一个新的库 -
步骤三:
去settings.py文件中修改配置
首先是修改templates文件夹的注册路径(部分代码)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
接着是static文件夹的路径注册
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
由于我们替换了auth组件的User表进行了字段扩展,所以需要在配置文件中修改使用的表
AUTH_USER_MODEL = 'app01.UserInfo'
接着是修改数据库的配置信息
DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'bbs1',
'HOST': '127.0.0.1',
'POST': 3306,
'USER': 'root',
'PASSWORD': '976863429',
'CHARSET': 'utf8'
}
}
ps:注册信息配置好后还要连接数据库
- 步骤五:
在models.py文件中编写表信息(根据项目分析中的字段来写)
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True)
avatar = models.FileField(upload_to='avatar/', default='avatar/default.jpg', verbose_name='用户头像')
register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True)
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True)
class Site(models.Model):
"""个人站点表"""
siti_name = models.CharField(verbose_name='站点名称', max_length=32)
siti_title = models.CharField(verbose_name='站点标题', max_length=32)
siti_theme = models.CharField(verbose_name='站点样式', max_length=32, null=True)
class Article(models.Model):
"""文章表"""
title = models.CharField(verbose_name='文章标题', max_length=32)
desc = models.CharField(verbose_name='文章简介', max_length=255)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 三个优化字段
comment_num = models.IntegerField(verbose_name='评论数', default=0)
up_num = models.IntegerField(verbose_name='点赞数', default=0)
down_num = models.IntegerField(verbose_name='点踩数', default=0)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
catagory = models.ForeignKey(to='Category',on_delete=models.CASCADE, null=True)
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag'),
null=True
)
class Category(models.Model):
"""文章分类表"""
name = models.CharField(verbose_name='分类名称', max_length=32)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
class Tag(models.Model):
"""文章标签表"""
name = models.CharField(verbose_name='标签名称', max_length=32)
site = models.ForeignKey(to='Site',on_delete=models.CASCADE, null=True)
class Article2Tag(models.Model):
"""这是文章表和标签表的多对多字段需要创建的表,这里我们使用半自动创建的方式创建多对多字段"""
article=models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE, null=True)
class UpAndDown(models.Model):
"""文章点赞点踩表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
is_up = models.BooleanField(verbose_name='点赞点踩')
class Comment(models.Model):
"""文章评论表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
content = models.TextField(verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True)
# 这里的self就是指代Comment,即指代自己所代表的表
编写好了之后执行数据库迁移操作
注册功能
功能概括
用户注册
1.渲染前端标签
2.校验用户数据
3.展示错误提示
ps:forms组件、modelform组件
单独开设py文件编写 解耦合!!!
具体代码
首先我们在路由表中添加注册页面的路由
path('register/', views.register_func, name='register_view'),
接着我们去视图层创建视图函数并创建对应的html文件
在编写注册功能的视图函数时,我们想到可以使用forms组件进行校验并给出错误提示,这样大大节省了代码.同时我们回顾forms组件的知识点,forms组件需要创建一个模型表,但是放在视图层中不合适,因此我们把他单独放到应用下的myforms.py文件中,内部代码如下:
from django import forms
from app01 import models
class RegisterForm(forms.Form):
"""用户注册form类"""
username = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={
'min_length': '用户名最短三位',
'max_length': '用户名最长八位',
'required': '用户名不能为空'
},
widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(min_length=3, max_length=8, label='密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空'
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(min_length=3, max_length=8, label='确认密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空'
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确'
},
widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})
)
# 用户头像单独校验 不使用校验类 其他字段自己根据需求自定义添加即可
# 钩子函数
# 局部钩子校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
res = models.UserInfo.objects.filter(username=username)
if res:
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
在创建好了forms模型表后我们在views.py文件中导入使用即可.
在编写注册的视图函数时我们需要根据forms组件校验的结果做出不同的返回结果
views.py中的代码
from django.shortcuts import render, HttpResponse, redirect
from app01 import myforms
from app01 import models
from django.http import JsonResponse
# Create your views here.
def register_func(request):
# 前后端ajax交互 通常采用字典作为交互对象
back_dict = {'code': 10000, 'msg': ''}
# 1.先产生一个空的form_obj
form_obj = myforms.RegisterForm()
if request.method == "POST":
form_obj = myforms.RegisterForm(request.POST) # username password confirm_password email csrfmiddlewaretoken
if form_obj.is_valid():
clean_data = form_obj.cleaned_data # 存储符合校验的数据 {username password confirm_password email}
# 将confirm_password键值对移除
clean_data.pop('confirm_password') # {username password email}
# 获取用户上传的头像文件
avatar_obj = request.FILES.get('avatar') # 用户有可能没有上传,但是我们在编写表的时候设置了默认值,因此需要通过下方的if判断添加数据,当没有头像上传的时候就使用默认值中的图片
if avatar_obj:
clean_data['avatar'] = avatar_obj # {username password email avatar }
# 创建用户数据
models.UserInfo.objects.create_user(**clean_data) # 上述处理字典的目的就是为了创建数据省事
back_dict['msg'] = '注册成功'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = form_obj.errors
return JsonResponse(back_dict)
return render(request, 'registerPage.html', locals())
html文件中的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% load static %}
<script src="{% static 'jquery-3.6.1.js' %}"></script>
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户注册</h2>
<form id="form"> <!--不使用form表单提交数据 但是用一下form标签 它有一个序列化功能-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group"> <!--目的是让多个获取用户数据的标签上下间距更大一些-->
<label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id自动获取渲染的标签id值-->
{{ form }}
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<!--用户头像自己编写相关标签获取-->
<div class="form-group">
<label for="myfile">头像
<img src="/static/img/default.jpg" alt="" width="120" id="myimg">
</label>
<input type="file" id="myfile" style="display: none">
</div>
<input type="button" id="subBtn" class="btn btn-primary btn-block" value="注册">
</form>
</div>
</div>
<script>
// 1.用户头像的实时展示
$('#myfile').change(function () {
// 1.产生一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
// 3.将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj);
// 等待文件阅读器对象加载完毕之后再修改src
myFileReaderObj.onload = function () {
// 4.修改img标签的src属性展示图片
$('#myimg').attr('src', myFileReaderObj.result)
}
})
// 2.给注册按钮绑定点击事件 发送ajax 携带了文件数据
$('#subBtn').click(function () {
// 1.先产生一个内置对象
let myFormDataObj = new FormData();
// 2.添加普通数据(单个单个的编写效率极低)
{#console.log($('#form').serializeArray()) // 可以一次性获取form标签内所有普通字段数据#} // [{},{},{},{}]
$.each($('#form').serializeArray(), function (index, dataObj) { // 对结果for循环 然后交给后面的函数处理
myFormDataObj.append(dataObj.name, dataObj.value) // {'name':'','value':''}
})
// 3.添加文件数据
myFormDataObj.append('avatar', $('#myfile')[0].files[0])
// 4.发送ajax请求
$.ajax({
url:'',
type:'post',
data:myFormDataObj,
contentType:false,
processData: false,
success:function (args) {
if(args.code === 10000){
window.location.href = args.url
}else{
{#console.log(args.msg)#}
let dataObj = args.msg;
// 如何针对性的渲染错误提示 {'username'} id_username
$.each(dataObj, function (k, msgArray) {
// 拼接标签的id值
let eleId = '#id_' + k
// 根据id查找标签 修改下面span标签的内容 并给父标签添加错误样式
$(eleId).next().text(msgArray[0]).parent().addClass('has-error')
})
}
}
})
})
// 3.给所有的input标签绑定获取焦点事件 移除错误样式
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error')
})
</script>
</body>
</html>
登录功能
知识点介绍
登陆功能今天只是学了一部分,除了用户名和密码,我们还需要实现验证码的功能.
关于验证码的图片会用到img标签的src属性知识点:
1.可以直接填写图片地址
2.还可以填写一个路由,会自动朝该路由发送get请求
如果返回的结果是图片的二进制数据,那么会自动渲染图片
pillow模块
pillow模块是用于生成图片的,我们产生的验证码就是用他产生的
带入代码如下:
from PIL import Image, ImageFont, ImageDraw
Image 产生图片
ImageFont 字体样式
ImageDraw 画笔对象
io模块
from io import BytesIO, StringIO
"""
BytesIO 在内存中临时存储 读取的时候以bytes格式为准
StringIO 在内存中临时存储 读取的时候以字符串格式为准
"""
登陆界面具体代码
首先也是在urls.py文件中注册网址后缀,这里我们把生成验证码的功能放到另一个视图函数中去了,所以需要注册两个路由.
# 用户登录功能
path('login/', views.login_func, name='login_view'),
# 图片验证码相关功能
path('get_code/', views.get_code_func)
接着去views.py文件中编写具体代码
def login_func(request):
return render(request, 'loginPage.html')
from PIL import Image, ImageFont, ImageDraw
"""
Image 产生图片
ImageFont 字体样式
ImageDraw 画笔对象
"""
from io import BytesIO, StringIO
'通过下方代码中的推导流程我们可以得知pillow模块虽然可以创建图片,但是他需要先保存图片才能调用,因此我们使用io模块存储数据'
"""
BytesIO 在内存中临时存储 读取的时候以bytes格式为准
StringIO 在内存中临时存储 读取的时候以字符串格式为准
"""
import random
def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_code_func(request):
# 1.推导步骤1:直接读取图片文件返回
# with open(r'D:\pythonProject03\BBS\avatar\555.jpg', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 2.推导步骤2:随机产生图片动态返回 第三方pillow模块
# img_obj = Image.new('RGB', (350, 35), 'green')
# with open(r'xxx.png', 'wb') as f:
# img_obj.save(f, 'png')
# with open(r'xxx.png', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 3.推导步骤3:针对图片的保存与读取做优化 内存管理器
# img_obj = Image.new('RGB', (350, 35), 'yellow')
# io_obj = BytesIO()
# img_obj.save(io_obj, 'png')
# return HttpResponse(io_obj.getvalue())
# 4.推导步骤4:图片颜色是可以随机变换的
img_obj = Image.new('RGB', (350, 35), get_random())
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% load static %}
<script src="{% static 'jquery-3.6.1.js' %}"></script>
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户登录</h2>
<div class="form-group">
<label for="name">用户名</label>
<input type="text" id="name" class="form-control">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" class="form-control">
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="code" class="form-control">
</div>
<div class="col-md-6">
<img src="/get_code/" alt="" width="350" height="35">
</div>
</div>
</div>
<input type="button" class="btn btn-success btn-block" value="登录">
</div>
</div>
</body>
</html>
通过上面的代码我们可以发现前端的代码是通过调用后端的视图函数对应的路由后缀获取图片的,这个时候我们在浏览器中检查代码也不能看到图片的名称,只能看到路由后缀.