BBS项目架构
数据库设计
用户表(用的是auth_user那张表,通过自定义继承AbstractUser)
phone 电话 avatar 头像 create_time 创建时间 #外键 blog 一对一个人站点表
个人站点表(副标题,摘要)
site_name 站点名 site_title 名言警句 site_theme 样式
标签表
name 标签名 #外键 blog 一对多个人站点 每个站点含有多个标签
分类表
name 分类名 #外键 blog 一对多个站点 每个站点含有多个标签
文章表
title 文章标题 desc 文章摘要 content 文章内容 create_time 文章创建时间 #外键 blog 一对多个人站点 一个站点下可以有多篇文章 tag 多对多标签 一个文章有多个标签,一个标签可以对应多篇文章 category 一对多分类 一个分类下面有多篇文章 #普通字段 #数据库设计优化(*******)设置这些就不用于点赞点踩表中查询数据 了,减少数据库查询量 #开一个事务,当你操作点赞点踩表的时候,下面这些数据也会变化 评论数 comment_num 普通字段 点赞数 up_num 普通字段 点踩数 down_num 普通字段
点赞点踩表
user 一对多用户表 一个评论是一个用户写的,一个用户能写多个评论 article 一对多文章表 comment 评论内容 create_time 创建时间 parent 一对多评论表(自关联) 父评论的id 如果有值说明你是子评论 如果没有值说明你是父评论
评论表
# 根评论和子评论分析 id user articel content commit 1 1 1 写的真好 null 2 1 2 写的真烂 null 3 2 2 你放屁,人家写的很好 2 4 1 2 你怎么骂人呢? 3 5 2 2 就骂你怎么了 4 6 3 2 你这个嘴太碎了 2 7 4 2 碎嘴子 2
需要创建的表
from django.db import models from django.contrib.auth.models import AbstractUser class UserInfo(AbstractUser): phone = models.CharField(max_length=32) # upload_to文件上传以后存放的路径 # FileField本质是varchar类型 # 坑() avatar = models.FileField(upload_to='avatar/', default='avatar/default.png') blog = models.OneToOneField(to='Blog',on_delete=models.CASCADE) class Blog(models.Model): site_title = models.CharField(max_length=32) site_name = models.CharField(max_length=32) # 每个人样式不同(文件地址) site_style = models.CharField(max_length=32) class Tag(models.Model): name = models.CharField(max_length=32) blog = models.ForeignKey(to='Blog',on_delete=models.CASCADE) class Category(models.Model): name = models.CharField(max_length=32) blog = models.ForeignKey(to='Blog',on_delete=models.CASCADE) class Article(models.Model): title = models.CharField(max_length=32) desc = models.CharField(max_length=128) # 大文本 content = models.TextField() create_time = models.DateTimeField(auto_now_add=True) # 关联关系 blog = models.ForeignKey(to='Blog',on_delete=models.CASCADE) category = models.ForeignKey(to='Category',on_delete=models.CASCADE) # 多对多关系 tag = models.ManyToManyField(to='Tag', through='TagToArticle', through_fields=('article', 'tag')) class TagToArticle(models.Model): tag = models.ForeignKey(to='Tag',on_delete=models.CASCADE) article = models.ForeignKey(to='Article',on_delete=models.CASCADE) class UpAndDown(models.Model): user = models.ForeignKey(to='UserInfo',on_delete=models.CASCADE) article = models.ForeignKey(to='Article',on_delete=models.CASCADE) # 实质存的时候,是0和1 is_up = models.BooleanField() create_time = models.DateTimeField(auto_now_add=True) class Commit(models.Model): user = models.ForeignKey(to='UserInfo',on_delete=models.CASCADE) article = models.ForeignKey(to='Article',on_delete=models.CASCADE) content = models.CharField(max_length=256) create_time = models.DateTimeField(auto_now_add=True) # 存父评论的id号 # commit_id=models.IntegerField() # commit=models.ForeignKey(to='Commit') # 自关联(不能叫表明小写) commit_id=models.ForeignKey(to='self',on_delete=models.CASCADE) # 踩了两个坑 avatar = models.FileField(upload_to='avatar/', default='avatar/default.png') commit_id=models.ForeignKey(to='self',on_delete=models.CASCADE) # 迁移(两条命令)
注册form组件编写
1 用户名,密码, 修改个人信息:邮箱,手机号,头像 2 用户名,密码,邮箱,头像 from django import forms from django.forms import widgets from blog import models from django.forms import ValidationError class RegisterForm(forms.Form): username = forms.CharField(required=True, max_length=18, min_length=3, label='用户名', error_messages={'required': '该字段必填', 'max_length': '最大长度为18', 'min_length': '最短为3'}, widget=widgets.TextInput(attrs={'class': 'form-control'})) password = forms.CharField(required=True, max_length=18, min_length=3, label='密码', error_messages={'required': '该字段必填', 'max_length': '最大长度为18', 'min_length': '最短为3'}, widget=widgets.PasswordInput(attrs={'class': 'form-control'})) re_password = forms.CharField(required=True, max_length=18, min_length=3, label='确认密码', error_messages={'required': '该字段必填', 'max_length': '最大长度为18', 'min_length': '最短为3'}, widget=widgets.PasswordInput(attrs={'class': 'form-control'})) email = forms.EmailField(required=True, label='邮箱', error_messages={'required': '该字段必填'}, widget=widgets.EmailInput(attrs={'class': 'form-control'})) # 用户名如果存在了,就不能注册 def clean_username(self): username = self.cleaned_data.get('username') # 去数据库查询 user = models.UserInfo.objects.filter(username=username).count() if user: # 不合法 raise ValidationError('用户名已经存在') else: return username # 校验两次密码是否一致 def clean(self): password = self.cleaned_data.get('password') re_password = self.cleaned_data.get('re_password') if re_password == password: return self.cleaned_data else: raise ValidationError('两次密码不一致')
注册功能页面搭建<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script> <title>注册</title> </head> <body> <div class="container-fluid"> <div class="row"> <h1 class="text-center">注册功能</h1> <div class="col-md-6 col-md-offset-3"> <form id="form"> {% csrf_token %} {% for foo in form %} <div class="form-group"> <label for="{{ foo.auto_id }}">{{ foo.label }}</label> {{ foo }} </div> {% endfor %} <div class="form-group"> <label for="id_myfile">头像 <img src="/static/img/default.png" alt="" id="id_img" height="80" width="80" style="margin-left: 10px"> </label> <input type="file" name="myfile" id="id_myfile" style="display: none"> </div> <div class="text-center"> {#注意这个坑#} <input type="button" value="注册" id="id_submit" class="btn btn-danger"> </div> </form> </div> </div> </div> </body> <script> //放文件的标签发生变化,我们把文件搞出来,放到img标签中 $("#id_myfile").change(function () { //借助于文件阅读器 const filereader = new FileReader() //把图片读到filereader对象中 //$('#id_myfile')[0].files[0] filereader.readAsDataURL($('#id_myfile')[0].files[0]) //$('#id_img').attr('src','https://account.cnblogs.com/images/registersideimg.png') //$('#id_img').attr('src',filereader.result) //这样不行,文件没读完 filereader.onload = function () { //文件完全读到文件阅读器以后,再执行 $('#id_img').attr('src', filereader.result) } }) $("#id_submit").click(function () { var formdata = new FormData() formdata.append('avatar', $('#id_myfile')[0].files[0]) #取文件对象 //第一种方式: /* formdata.append('username', $('#id_name').val()) formdata.append('password', $('#id_password').val()) formdata.append('re_password', $('#id_re_password').val()) formdata.append('email', $('#id_email').val()) */ //第二种方式: var ser = $('#form').serializeArray() //[{name:name,value:lqz}, {name:password],value:123}, {name:re_password],value:123}, {name:email],value:123@qq.com}] //console.log(ser) $.each(ser, function (k, v) { //console.log(v.name) //console.log(v.value) formdata.append(v.name, v.value) })
$.ajax({ url: '/register/', method: 'post', processData: false, contentType: false, data: formdata, success: function (data) { if (data.code == 100) { console.log(data.msg) //js控制的跳转 location.href=data.url } else { //有错误,需要渲染页面 } } }) }) </script> </html>
头像实时显示
$("#id_myfile").change(function () { //借助于文件阅读器 const filereader = new FileReader() //把图片读到filereader对象中 //$('#id_myfile')[0].files[0] filereader.readAsDataURL($('#id_myfile')[0].files[0]) //$('#id_img').attr('src','https://account.cnblogs.com/images/registersideimg.png') //$('#id_img').attr('src',filereader.result) //这样不行,文件没读完 filereader.onload = function () { //文件完全读到文件阅读器以后,再执行 $('#id_img').attr('src', filereader.result) } })
头像修改后端
第一种方法:(比较麻烦,需要再自己手动操作存一次文件,用update存,只是将文件的路径存进了avatar并没有将图片文件存进去)
def chang_img(request): res = {'code': 100, 'msg': '修改成功'} print(11111) file = request.FILES.get('avatar') if file:
#存头像到文件夹 img=Image.open(file) img.save('media/avatar/'+file.name) file = 'avatar/' + file.name print(file, type(file)) obj=models.UserInfo.objects.filter(username=request.user.username).update(avater=file) return JsonResponse(res) res['code'] = 101 res['msg'] = '修改失败' return JsonResponse(res)
第二种方法:直接保存图片对象即可
def update_head(request):
if request.method == 'GET':
return render(request, 'backend/update_head.html')
else:
head = request.FILES.get('myfile')
# 方式一
request.user.avatar = head
request.user.save()
return redirect('/')
头像修改前端
$('#id_myfile').change(function () { //借助于阅读器 const filereader = new FileReader() //把图片读到filereader对象中 filereader.readAsDataURL($('#id_myfile')[0].files[0]) filereader.onload = function () { console.log(1111112222) //文件完全读到文件阅读器以后再执行 $('#id_img').attr('src', filereader.result) } }) $('#changeimg').click(function () { var formdata = new FormData() formdata.append('avatar', $('#id_myfile')[0].files[0]) console.log(333333) formdata.append('csrfmiddlewaretoken', '{{ csrf_token }}') console.log(formdata) $.ajax({ url: '/chang_img/', method: 'post', data: formdata, processData: false, contentType: false, success: function (data) { if (data.code === 100) { alert(data.msg) } else { alert(data.msg) } } })
注册功能 利用form组件来校验登录信息是否正确,并且渲染页面
1 文件输入框设置了id='myfile'和隐藏文件输入框,在lable标签设置for='myfile',那么点击头像两个字就弹出文件输入框的效果
<label for="myfile">头像 设置默认头像图片 <img src="/static/img/default.jpg" alt="" height="80" style="margin-left: 20px" id="img"> </label> <input type="file" id="myfile" style="display: none"> #文件输入框
注册功能完成(后端)
def register(request):
if request.method == 'GET':
form = RegisterForm()
return render(request, 'register.html', context={'form': form})
elif request.method == 'POST':
# 校验数据是否合法
res = {'code': 100, 'msg': '注册成功'}
form = RegisterForm(data=request.POST)
if form.is_valid():
# 保存到数据库
data = form.cleaned_data # {username:lqz,password:123,re_password:123,email:3@qq.com}
data.pop('re_password')# {username:lqz,password:123,email:3@qq.com}
file=request.FILES.get('avatar')
if file:
data['avatar']=file # {username:lqz,password:123,email:3@qq.com,'avatar':文件对象}
print(data)
models.UserInfo.objects.create_user(**data)
# res['url']='/index/'
res['url']='http://www.baidu.com'
'''
FileField自动干了这个事
with open('media/avatar/%s'%file.name,'wb') as f:
for line in file:
f.write(line)
path='media/avatar/%s'%file.name
'''
# models.UserInfo.objects.create_user(username=username,password=password,email=email,avatar=path)
# 返回,会被ajax接收到
return JsonResponse(res)
else:
# 数据校验不通过
res['code'] = 101 # 101表示注册失败
res['msg'] = '数据验证失败'
res['error'] = form.errors
return JsonResponse(res)
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
<title>注册</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<h1 class="text-center">注册功能</h1>
<div class="col-md-6 col-md-offset-3">
<form id="form">
{% csrf_token %}
{% for foo in form %}
<div class="form-group">
<label for="{{ foo.auto_id }}">{{ foo.label }}</label>
{{ foo }}
</div>
{% endfor %}
<div class="form-group">
<label for="id_myfile">头像
<img src="/static/img/default.png" alt="" id="id_img" height="80" width="80"
style="margin-left: 10px">
</label>
<input type="file" name="myfile" id="id_myfile" style="display: none">
</div>
<div class="text-center">
{#注意这个坑#}
<input type="button" value="注册" id="id_submit" class="btn btn-danger">
</div>
</form>
</div>
</div>
</div>
</body>
<script>
//放文件的标签发生变化,我们把文件搞出来,放到img标签中
$("#id_myfile").change(function () {
//借助于文件阅读器
const filereader = new FileReader()
//把图片读到filereader对象中
//$('#id_myfile')[0].files[0]
filereader.readAsDataURL($('#id_myfile')[0].files[0])
//$('#id_img').attr('src','https://account.cnblogs.com/images/registersideimg.png')
//$('#id_img').attr('src',filereader.result) //这样不行,文件没读完
filereader.onload = function () {
//文件完全读到文件阅读器以后,再执行
$('#id_img').attr('src', filereader.result)
}
})
$("#id_submit").click(function () {
var formdata = new FormData()
formdata.append('avatar', $('#id_myfile')[0].files[0])
//第一种方式:
/*
formdata.append('username', $('#id_name').val())
formdata.append('password', $('#id_password').val())
formdata.append('re_password', $('#id_re_password').val())
formdata.append('email', $('#id_email').val())
*/
//第二种方式:(推荐)
var ser = $('#form').serializeArray()
相当于做了下面的事
//[{name:name,value:lqz}, {name:password],value:123}, {name:re_password],value:123}, {name:email],value:123@qq.com}]
//console.log(ser)
$.each(ser, function (k, v) {
//console.log(v.name)
//console.log(v.value)
formdata.append(v.name, v.value)
})
$.ajax({
url: '/register/',
method: 'post',
processData: false,
contentType: false,
data: formdata,
success: function (data) {
if (data.code == 100) {
console.log(data.msg)
//js控制的跳转
location.href = data.url
} else {
//有错误,需要渲染页面
}
}
})
})
</script>
</html>
登录功能实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录页面</title> {% load static %} <script src="{% static 'jquery-3.3.1/jquery-3.3.1.min.js' %}"></script> <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}"> <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script> {# <script src="jQuery.js"></script>#} </head> <body> <div class="container-fluid"> <div class="row"> <h1 class="text-center">登录功能</h1> <div class="col-md-6 col-md-offset-3"> <form action=""> {% csrf_token %} <div class="form-group"> <lable for=""> 用户名</lable> <input type="text" name="username" class="form-control"> </div> <div class="form-group"> <lable> 密码</lable> <input type="text" name="password" class="form-control"> </div> <div class="form-group"> <lable> 验证码</lable> <div class="row"> <div class="col-md-4"> <input type="text" name="valid_code" class="form-control"> </div> <div class="col-md-6"> <img src="/get_valid/" alt="" height="40" width="400" id="id_valid_code"> </div> </div> </div> <div class="text-center"> <input type="button" value="登录" id="id_submit" class="btn btn-danger"> <span class="error text-danger" style="margin-left: 10px"> </span> </div> </form> </div> </div> </div> </body> <script> $('#id_valid_code').click(function () { var url = $('#id_valid_code')[0].src $('#id_valid_code')[0].src = url+'?' }) $('#id_submit').click(function () { $.ajax({ url:'/login/', method:'post', data:{ {#通过name取值#} 'username':$('[name=username]').val(), 'password':$('[name=password]').val(), 'valid_code':$('[name=valid_code]').val(), 'csrfmiddlewaretoken':'{{ csrf_token }}', }, success:function (data) { console.log(data) if (data.code === '100'){ location.href=data.url }else { $('.error').html(data.msg) } } }) }) </script> </html>
后端
def login(request): if request.method == 'GET': return render(request, 'login.html') else: print(111111111111) res = {'code':'100','msg':None} username = request.POST.get('username') password = request.POST.get('password') valid_code = request.POST.get('valid_code') if valid_code.upper() == request.session.get('valid_code').upper(): user = auth.authenticate(username=username,password=password) if user: #验证成功后一定要写这一句,这样在任何地方都可以拿到request.user auth.login(request,user) res['msg']='登录成功' res['url']='/index/' else: res['code']='101' res['msg']='用户名或密码错误' else: res['code']='102' res['msg']='验证码错误' print(22222) return JsonResponse(res)
首页代码轮播图
<div class="middle_content col-md-7"> <div class="lbt"> <div id="carousel-example-generic" class="carousel slide" data-ride="carousel"> <!-- Indicators --> <ol class="carousel-indicators"> <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li> <li data-target="#carousel-example-generic" data-slide-to="1"></li> <li data-target="#carousel-example-generic" data-slide-to="2"></li> </ol> <!-- Wrapper for slides --> <div class="carousel-inner" role="listbox"> <div class="item active"> <a href="{{ ll.0.url }}"><img src="{{ ll.0.img_url }}" alt="..." class="img"></a> <div class="carousel-caption"> {{ll.0.msg}} </div> </div> <div class="item"> <a href="http://www.mzitu.com"><img src="{{ ll.1.img_url }}" alt="..." class="img"></a> <div class="carousel-caption"> {{ll.1.msg}} </div> </div> <div class="item"> <a href="{{ll.1.url}}"><img src="{{ ll.2.img_url }}" alt="..." class="img" height='802px' width="306px"></a> <div class="carousel-caption"> 点我看美女 </div> </div> </div> <!-- Controls --> <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev"> <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next"> <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div> </div>
首页文章展示代码
<div class="article"> {% for article in article_list %} <hr> <div class="media"> <h4 class="media-heading"><a href="/{{article.blog.userinfo.username}}/article/{{ article.id }}">{{ article.title }}</a></h4> <div class="media-heading">{{ article.desc }} </div> <div class="media-left"> <a href=""> <img class="media-object" src="/media/{{ article.blog.userinfo.avater }}" alt="..." width="60" height="60"> </a> </div> <div class="article_bottom"><span><a href="">{{ article.blog.userinfo.username }}</a></span> <span>{{ article.create_time | date:'Y-m-d H-i-s' }}</span> <span><i class="fa fa-stethoscope fa-lg"></i>   {{ article.up_num }}    </span><span class="glyphicon glyphicon-comment"><i class="fa fa-flickr1 fa-lg"> </i> {{ article.commit_num }} </span> </div> </div> {% endfor %} </div>
# 步骤 -在app下创建一个包,templatetags -任意新建一个py文件my_tag.py -导入library register = library.Library() -写一个函数,使用inclusion_tag装饰 @register.inclusion_tag('left.html') def left(username): user = models.UserInfo.objects.filter(username=username).first() res_category = models.Category.objects.filter(blog=user.blog).annotate(num=Count('article__id')).values_list( 'name', 'num', 'id') res_tag = models.Tag.objects.filter(blog=user.blog).annotate(num=Count('article__id')).values_list('name', 'num', 'id') res_month = models.Article.objects.filter(blog=user.blog).annotate(month=TruncMonth('create_time')).values( 'month').annotate(c=Count('id')).order_by('-month').values_list('month', 'c') return {'name':username,'res_category': res_category, 'res_tag': res_tag, 'res_month': res_month} -写一个模板 <div> <div class="panel panel-danger"> <div class="panel-heading"> <h3 class="panel-title">我的标签</h3> </div> <div class="panel-body"> {% for tag in res_tag %} <p><a href="/{{ name }}/tag/{{ tag.2 }}.html">{{ tag.0 }}({{ tag.1 }})</a></p> {% endfor %} </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">随笔分类</h3> </div> <div class="panel-body"> {% for category in res_category %} <p><a href="/{{ name }}/category/{{ category.2 }}.html">{{ category.0 }}({{ category.1 }})</a></p> {% endfor %} </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">随笔档案</h3> </div> <div class="panel-body"> {% for month in res_month %} <p> <a href="/{{ name }}/archive/{{ month.0|date:'Y/m' }}.html">{{ month.0|date:'Y年m月' }}({{ month.1 }})</a> </p> {% endfor %} </div> </div> </div> -使用,在指定的位置 {% load mytag %} {% left name %}
个人站点(前端):
1 {% extends 'base.html' %} 2 3 {% block title %} 4 {{ user.username }}-博客园 5 {% endblock %} 6 {% block content %} 7 8 <div class="article"> 9 {% for article in article_list %} 10 <hr> 11 <div class="media"> 12 <h4 class="media-heading"><a href="/{{ user.username }}/article/{{ article.id }}.html">{{ article.title }}</a></h4> 13 14 <div class="media-body"> 15 {{ article.desc }} 16 </div> 17 <div class="article_bottom pull-right"> 18 <span>posted@</span> 19 <span>{{ article.create_time|date:'Y-m-d H-i-s' }}</span> 20 <span>{{ user.username }}</span> 21 <span><i class="fa fa-stethoscope fa-lg"></i> {{ article.up_num }}</span><span 22 class="glyphicon glyphicon-comment"><i 23 class="fa fa-flickr1 fa-lg"></i> {{ article.commit_num }}</span> 24 <span><a href="">编辑</a></span> 25 </div> 26 </div> 27 {% endfor %} 28 29 30 </div> 31 {% endblock %}
个人站点后端
def site(request, name, **kwargs): # 分三种情况(根据标签过滤,根据分类过滤,根据时间过滤) user = models.UserInfo.objects.filter(username=name).first() if user: article_list = user.blog.article_set.all() # 根据不同情况对article_list进行过滤,article_list是queryset对象,可以继续filter query = kwargs.get('query', None) if query == 'category': # 说明走的是过滤的路由 condition = kwargs.get('condition') article_list = article_list.filter(category_id=condition) elif query == 'tag': condition = kwargs.get('condition') article_list = article_list.filter(tag__id=condition) elif query == 'archive': year, month = kwargs.get('condition').split('/') # 2020/09 article_list = article_list.filter(create_time__year=year, create_time__month=month) return render(request, 'site.html', locals()) else: return render(request, 'error.html')