BBS项目(一): 表设计 注册功能 登录功能 生成随机验证码
表设计
# 仿造博客园项目
核心:文章的增删改查
# 表分析
先确定表的数量 再确定表的基础字段 最后确定表的外键字段
(可能公司里用不到外键,因为外键强耦合)
'''
接手一个新项目:
1.去数据库查看表关系、表结构 把表关系弄清楚了业务逻辑也就清楚了。
2.如果是一个django项目 可以查看model.py文件
'''
1.确定表的数量
仿造博客园网站,梳理出以下数据表:
首先找最熟悉的表:
1.用户表
每个用户个人站点的名字,样式都不一样,所以需要一张表存用户站点的信息:
2.个人站点表
个人站点里面存的是文章表:
3.文章表
文章的右侧是分类(想到文章就会想到文章分类):
4.文章分类表
标签用于描述某一篇文章内容所属的范围
(可以给很多的文章贴相同的标签,也一个文章贴多个标签):
5.标签表
文章发布出来之后,可以对文章进行点赞点踩:
6.点赞点踩表
文章发布之后,可以进行评论。
评论表应该包括 ---> 评论的内容 评论的用户 评论的时间
7.文章评论表
# 评论、点赞点踩都是对于文章的操作,为什么不用文章表来存储这些数据?
1. 我们希望文章表就只有文章相关的数据,评论和点赞实际也不属于文章的一部分,对于评论点赞希望可以解耦合。
2. 数据库查文章表的时候会把对这条文章的评论一同查询出来,这样有点浪费数据库资源。
3. 而且评论和点赞可能都有不同的功能要做。也就是对于评论可能还要新建一张表,然后多加几个字段。
2.确定表的基础字段
'''下列表字段设计仅供参考 你可以有更多的想法'''
用户表
替换auth_user表并扩展额外的字段
扩展三个额外字段:
1.电话号码
2.头像
3.注册时间
个人站点表
1.站点名称
2.站点标题
3.站点样式(每个人都拥有自己的CSS文件)
4.站点样式2(每个人都拥有自己的js文件)
文章表
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
"""
id=1 是 根评论
id=2 是 id=1的子评论
id=3 是 id=2的子评论
3.确定表的外键字段
外键字段
用户表
用户与个人站点是一对一外键关系
个人站点表
文章表
文章表与个人站点表是一对多外键关系
文章表与文章分类表是一对多外键关系
文章表与文章标签表是多对多外键关系
'''
数据库字段优化设计:我们想统计文章的评论数 点赞数
通过文章数据跨表查询到文章评论表中对应的数据统计即可
但是文章需要频繁的展示 每次都跨表查询的话效率极低
我们在文章表中再创建三个普通字段
之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三 个普通字段即可
文章表可以添加visits字段 记录文章访问量
'''
文章表优化字段:
文章评论数
文章点赞数
文章点踩数
文章分类表
文章分类与个人站点是一对多外键关系
文章标签表
文章标签与个人站点是一对多外键关系
表关系图
from django.db import models
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True)
avatar = models.FileField(verbose_name='用户头像', upload_to='avatar/', default='avatar/default.png')
register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True)
is_alive = models.BooleanField(verbose_name='用户是否注销账号') # 传布尔值存 0或者1
# 外键字段
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True)
class Site(models.Model):
"""个人站点表"""
site_name = models.CharField(verbose_name='站点名称', max_length=32)
site_title = models.CharField(verbose_name='站点标题', max_length=32)
site_theme = models.CharField(verbose_name='站点样式', max_length=32, null=True)
site_css = models.FileField(verbose_name='用户个人css样式', upload_to='user_theme/css', null=True)
site_js = models.FileField(verbose_name='用户个人js脚本', upload_to='user_theme/js', null=True)
site_img = models.FileField(verbose_name='用户上传的图片', upload_to='user_theme/img', 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)
visits = models.IntegerField(verbose_name='访问量', default=0)
# 三个优化字段
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)
category = models.ForeignKey(to='Category', on_delete=models.CASCADE, null=True)
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag')
)
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 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 UpAndDown(models.Model):
"""点赞点踩表"""
user = models.ForeignKey(verbose_name='进行点赞操作的用户', to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(verbose_name='被点赞的文章', to='Article', on_delete=models.CASCADE, null=True)
is_up = models.BooleanField(verbose_name='点赞点踩') # 传布尔值存 0或者1
class Comment(models.Model):
"""文章评论表"""
user = models.ForeignKey(verbose_name='写评论的用户', to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(verbose_name='被评论的文章', to='Article', on_delete=models.CASCADE, null=True)
content = models.TextField(verbose_name='评论内容')
comment_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
# 自关联
parent = models.ForeignKey(verbose_name='根评论主键', to='self', on_delete=models.CASCADE, null=True)
'''
多对多虚拟字段在文章表 所以through_field第一个写文章表的字段 manytomany。
'''
项目初建流程备忘
- 修改templates
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ], # 修改成这样
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
- 配置静态文件
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
- 连接mysql配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 引擎
'NAME': 'bbsday01', # 库名
'HOST': '127.0.0.1', # IP地址
'PORT': 3306, # 端口
'USER': 'root', # mysql用户名
'PASSWORD': '123', # 用户密码
'CHARSET': 'utf8', # 编码
}
}
- 创建app、注册app
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01', # 注册自己的app
]
- 配置用户表
AUTH_USER_MODEL= 'app01.UserInfo'
- 创建头像目录avatar/
- 创建用户样式目录user_theme/
-
创建表的基础字段
-
创建表的外键字段
-
创表之后 查看userinfo是否继承auth表的字段
注册功能
# 流程
1.路由(可以做路由分发)
2.视图
3.html网页
# 用户注册
1.渲染前端标签
2.获取用户数据
3.校验用户数据
4.展示错误提示
ps:forms组件、modelform组件
'''
不要在view.py编写form组件
而是单独开设py文件编写form组件 解耦合!!!
'''
form组件:
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'})
)
'''
注意:
1. 'required' 'invalid' 都是form组件生成错误信息字典里面固定的键 校验失败时,form组件默认会生成错误提示信息。 这里使用error_messages参数,用我们的错误信息替代form组件默认生成的错误信息。
2.
对应关系:
CharField TextInput type='text'
CharField PasswordInput type='password'
EmailField EmailInput type='email'
'''
# 用户头像单独校验 不使用校验类 其他字段自己根据需求自定义添加即可
# 钩子函数
# 局部钩子校验用户名是否已存在
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
registerPage:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/jquery3.6.1.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'layui/layui.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">
<div>
上传头像:
<img src="/static/img/default.png" alt="" id="myimg" style="width: 100px;
margin: 20px ;border-radius: 50%; overflow: hidden;">
</div>
</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>
register_func:
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 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())
登录功能
img标签的src属性
1.可以直接填写图片地址
2.还可以填写一个路由 会自动朝该路由发送get请求
如果结果是图片的二进制数据 那么自动渲染图片
pip install pillow -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
生成随机验证码
后端:
def login_func(request):
return render(request, 'loginPage.html')
from PIL import Image, ImageFont, ImageDraw
"""
Image 产生图片
ImageFont 字体样式
ImageDraw 画笔对象
"""
from io import BytesIO, StringIO
"""
BytesIO 在内存中临时存储 读取的时候以bytes格式为准
StringIO 在内存中临时存储 读取的时候以字符串格式为准
"""
import random
def get_code_func(request):
img_obj = Image.new(mode="RGB", size=(350, 35), color="white") # 产生画布 (模式, 画布尺寸, 画布颜色)
brush_obj = ImageDraw.Draw(img_obj) # 产生一个画笔对象
img_font = ImageFont.truetype('static/fonts/111.ttf', 30) # 产生字体 (字体路径,字体尺寸)
# 在画布上产生随机噪点
def point_site():
"""根据画布宽高,计算噪点的位置"""
return random.randint(0, img_obj.width), random.randint(0, img_obj.height)
def point_color():
"""生成噪点的RGB颜色"""
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
for i in range(img_obj.width * img_obj.height // 8): # 一次循环画一个噪点
brush_obj.point(point_site(), point_color()) # (点的位置,点的RGB颜色)
# 产生随机验证码
code = ''
for i in range(4):
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_lower, random_upper, random_int])
brush_obj.text((i * 50 + 60, -2), tmp, point_color(), img_font)
code += tmp
# 将验证码保存到session表
request.session['code'] = code
# 在内存中临时存储
io_obj = BytesIO()
img_obj.save(io_obj, 'png') # 将画布保存在内存
return HttpResponse(io_obj.getvalue())
前端:loginPage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/jquery3.6.1.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'layui/layui.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" id="change_code">
</div>
</div>
</div>
<input type="button" class="btn btn-success btn-block" value="登录">
</div>
</div>
</body>
<script>
// 1.验证码动态刷新
$('#change_code').click(function (){
var num = Math.ceil(Math.random() * 10000);//生成一个随机数(防止缓存)
console.log(num)
$(this).attr('src', "/get_code?num=" + num);
})
</script>
</html>