BBS(仿造博客园项目)
项目开发基本流程
|  | 1.需求分析 | 
|  | 2.架构设计 | 
|  | 3.分组开发 | 
|  | 4.提交测试 | 
|  | 5.交付上线 | 
项目流程
|  | 仿造博客园项目 | 
|  | 核心:文章的增删改查 | 
|  | 表分析 | 
|  | 先确定表的数量 再确定表的基础字段 最后确定表的外键字段 | 
|  | 1.用户表 | 
|  | 2.个人站点表 | 
|  | 3.文章表 | 
|  | 4.文章分类表 | 
|  | 5.文章标签表 | 
|  | 6.点赞点踩表 | 
|  | 7.文章评论表 | 
|  | 基础字段分析 | 
|  | '''下列表字段设计仅供参考 你可以有更多的想法''' | 
|  | 用户表 | 
|  | 替换auth_user表并扩展额外的字段 | 
|  | 电话号码、头像、注册时间 | 
|  | 个人站点表 | 
|  | 站点名称(jason\lili\kevin) | 
|  | 站点标题(努力奋斗去他妹的) | 
|  | 站点样式(css文件) | 
|  | 文章表 | 
|  | 文章标题 | 
|  | 文章简介 | 
|  | 文章内容 | 
|  | 发布时间 | 
|  | 文章分类表 | 
|  | 分类名称 | 
|  | 文章标签表 | 
|  | 标签名称 | 
|  | 点赞点踩表:记录哪个用户给哪篇文章点了推荐(赞)还是反对(踩) | 
|  | 用户字段(用户主键)>>>:外键字段 | 
|  | 文章字段(文章主键)>>>:外键字段 | 
|  | 点赞点踩 | 
|  | 文章评论表:记录哪个用户给哪篇文章评论了什么内容 | 
|  | 用户字段(用户主键)>>>:外键字段 | 
|  | 文章字段(文章主键)>>>:外键字段 | 
|  | 评论内容 | 
|  | 评论时间 | 
|  | 外键字段(自关联) | 
|  | """ | 
|  | id	user_id  article_id  content parent_id | 
|  | 1    1      1       哈哈哈   null | 
|  | 2   2      1        哈你妹   1 | 
|  | 3   3      1        讲文明   2 | 
|  | """ | 
|  | 评论点赞点踩表:记录哪个用户给那篇文章的哪条评论点赞或点踩 | 
|  | 用户字段(用户主键)>>>:外键字段 | 
|  | 文章字段(文章主键)>>>:外键字段 | 
|  | 评论字段(评论主键)>>>:外键字段 | 
|  | 点赞点踩 | 
|  | 外键字段 | 
|  | 用户表 | 
|  | 用户与个人站点是一对一外键关系 | 
|  |  | 
|  | 个人站点表 | 
|  |  | 
|  | 文章表 | 
|  | 文章表与个人站点表是一对多外键关系 | 
|  | 文章表与文章分类表是一对多外键关系 | 
|  | 文章表与文章标签表是多对多外键关系 | 
|  | ''' | 
|  | 数据库字段优化设计:我们想统计文章的评论数 点赞数 | 
|  | 通过文章数据跨表查询到文章评论表中对应的数据统计即可 | 
|  | 但是文章需要频繁的展示 每次都跨表查询的话效率极低 | 
|  | 我们在文章表中再创建三个普通字段 | 
|  | 之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三			个普通字段即可 | 
|  | ''' | 
|  | 文章评论数 | 
|  | 文章点赞数 | 
|  | 文章点踩数 | 
|  |  | 
|  |  | 
|  | 文章分类表 | 
|  | 文章分类与个人站点是一对多外键关系 | 
|  |  | 
|  | 文章标签表 | 
|  | 文章标签与个人站点是一对多外键关系 | 

注册功能
|  | 用户注册 | 
|  | 1.渲染前端标签 | 
|  | 2.校验用户数据 | 
|  | 3.展示错误提示 | 
|  | ps:forms组件、modelform组件 | 
|  |  | 
|  | 单独开设py文件编写 解耦合!!! | 
登录功能
|  | img标签的src属性 | 
|  | 1.可以直接填写图片地址 | 
|  | 2.还可以填写一个路由 会自动朝该路由发送get请求 | 
|  | 如果结果是图片的二进制数据 那么自动渲染图片 | 
|  |  | 
|  |  | 
|  | pip install pillow -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com | 
创建表相关代码
|  | from django.db import models | 
|  |  | 
|  |  | 
|  | from django.contrib.auth.models import AbstractUser | 
|  |  | 
|  |  | 
|  | class UserInfo(AbstractUser): | 
|  | """用户表,进行扩展,自定义字段""" | 
|  | phone = models.CharField(max_length=32, verbose_name='手机号', null=True) | 
|  | avatar = models.FileField(upload_to='avatar/', default='avatar/default.png', verbose_name='头像') | 
|  | register_time = models.DateTimeField(auto_now_add=True, verbose_name='注册时间') | 
|  |  | 
|  | """外键字段""" | 
|  | """用户表与个人站点表一对一关系""" | 
|  | site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  |  | 
|  | class Site(models.Model): | 
|  | """个人站点表""" | 
|  | site_name = models.CharField(max_length=32, verbose_name='站点名称') | 
|  | site_title = models.CharField(max_length=32, verbose_name='站点标题') | 
|  | site_theme_css = models.TextField(verbose_name='站点css', null=True) | 
|  | site_theme_js = models.TextField(verbose_name='站点js', null=True) | 
|  | site_theme_html = models.TextField(verbose_name='站点html', null=True) | 
|  | site_publish_info = models.TextField(verbose_name='公告', null=True) | 
|  |  | 
|  |  | 
|  | class Article(models.Model): | 
|  | """文章表""" | 
|  | title = models.CharField(max_length=32, verbose_name='文章标题') | 
|  | summary = models.TextField(verbose_name='文章摘要') | 
|  | content = models.TextField(verbose_name='文章内容') | 
|  | publist_time = models.DateTimeField(auto_now_add=True, verbose_name='文章发布时间') | 
|  |  | 
|  | """数据库字段优化""" | 
|  | """因为经常要统计下面字段数量,虽然跨表可以查询,但是浪费数据库资源,所有在进行数据增加或删除数据,对下面字段进行数据的修改 | 
|  | 以达到,减少数据库资源消耗,而进行优化的目的 | 
|  | """ | 
|  | up_num = models.IntegerField(verbose_name='点赞数', default=0) | 
|  | down_num = models.IntegerField(verbose_name='点踩数', default=0) | 
|  | comment_num = models.IntegerField(verbose_name='评论数', default=0) | 
|  |  | 
|  | """外键字段""" | 
|  | site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) | 
|  | classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  | """多对多字段,一个文章可以有很多标签,一个标签页可以有很多文章 | 
|  | 采用半自动创建多对多 | 
|  | """ | 
|  | labels = models.ManyToManyField(to='Label', | 
|  | through='Article2Label', | 
|  | through_fields=('article', 'label')) | 
|  |  | 
|  |  | 
|  | class Article2Label(models.Model): | 
|  | """多对多手动第三张表,后期可以扩展字段""" | 
|  | article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True) | 
|  | label = models.ForeignKey(to='Label', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  |  | 
|  | class Classify(models.Model): | 
|  | """文章分类表""" | 
|  | name = models.CharField(max_length=32, verbose_name='分类名称') | 
|  | site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  |  | 
|  | class Label(models.Model): | 
|  | """文章标签表""" | 
|  | name = models.CharField(max_length=32, verbose_name='文章标签表') | 
|  | site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  |  | 
|  | class UpAndDownArticle(models.Model): | 
|  | """文章点赞点踩表""" | 
|  | """记录:哪个用户给那篇文章点赞或点踩""" | 
|  | user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE) | 
|  | article = models.ForeignKey(to='Article', on_delete=models.CASCADE) | 
|  | is_up = models.BooleanField(verbose_name='点赞点踩') | 
|  |  | 
|  |  | 
|  | class Comment(models.Model): | 
|  | """文章评论表""" | 
|  | """记录:哪个用户给那片文章评论的内容与时间""" | 
|  | user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE) | 
|  | article = models.ForeignKey(to='Article', on_delete=models.CASCADE) | 
|  | content = models.TextField(verbose_name='评论内容') | 
|  | comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间') | 
|  |  | 
|  | """评论显示什么浏览器,以及评论者的ip""" | 
|  | user_agent = models.CharField(max_length=64, verbose_name='用户评论浏览器') | 
|  | user_ip = models.TextField(verbose_name='用户ip/ipv4/ipv6') | 
|  |  | 
|  | """自关联字段""" | 
|  | """在有时侯,某些字段需要关联所在表的数据就需要使用到自关联字段 | 
|  | id  user    content     parent | 
|  | 1   1       哈哈哈       null | 
|  | 2   2       不要         1 | 
|  | 3   3       你怎么管这么多 2 | 
|  | """ | 
|  | parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True) | 
|  |  | 
|  | """数据库字段优化""" | 
|  | up_num = models.IntegerField(verbose_name='评论被点赞数', default=0) | 
|  | down_num = models.IntegerField(verbose_name='评论被点踩', default=0) | 
|  |  | 
|  |  | 
|  | class UpAndDownComment(models.Model): | 
|  | """评论点赞点踩表""" | 
|  | """记录:哪个用户给那篇文章里的哪个评论点赞或点踩""" | 
|  | user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE) | 
|  | article = models.ForeignKey(to='Article', on_delete=models.CASCADE) | 
|  | comment = models.ForeignKey(to='Comment', on_delete=models.CASCADE) | 
|  | is_up = models.BooleanField(verbose_name='点赞点踩') | 
|  |  | 
注册功能相关代码
|  | from django import forms | 
|  | from django.forms import widgets | 
|  |  | 
|  | from BBS import models | 
|  |  | 
|  |  | 
|  | class Register_from(forms.Form): | 
|  | """用户注册forms类""" | 
|  |  | 
|  | username = forms.CharField(max_length=16, min_length=6, label='用户名', | 
|  | error_messages={ | 
|  | 'max_length': '用户名最长为16位', | 
|  | 'min_length': '用户名最短为6位', | 
|  | 'required': '用户名不能为空' | 
|  | }, | 
|  | widget=widgets.TextInput(attrs={'class': 'form-control'}) | 
|  | ) | 
|  | password = forms.CharField(max_length=16, min_length=6, label='密码', | 
|  | error_messages={ | 
|  | 'max_length': '密码最长为16位', | 
|  | 'min_length': '密码最短为6位', | 
|  | 'required': '密码不能为空' | 
|  | }, | 
|  | widget=widgets.PasswordInput(attrs={'class': 'form-control'})) | 
|  | confirm_password = forms.CharField(max_length=16, min_length=6, label='确认密码', | 
|  | error_messages={ | 
|  | 'max_length': '密码最长为16位', | 
|  | 'min_length': '密码最短为6位', | 
|  | 'required': '密码不能为空' | 
|  | }, | 
|  | widget=widgets.PasswordInput(attrs={'class': 'form-control'})) | 
|  | email = forms.EmailField(label='邮箱', error_messages={ | 
|  | 'required': '邮箱不能为空' | 
|  | }, | 
|  | widget=widgets.EmailInput(attrs={'class': 'form-control'}) | 
|  | ) | 
|  |  | 
|  | def clean_username(self): | 
|  | """局部钩子校验用户名是否已存在""" | 
|  | username = self.cleaned_data.get('username') | 
|  | user_obj = models.UserInfo.objects.filter(username=username) | 
|  | if user_obj: | 
|  | self.add_error('username', '用户已存在') | 
|  | return username | 
|  |  | 
|  | def clean(self): | 
|  | """全局钩子,因为要使用到两个数据所以使用全局钩子,进行两次密码一致性的校验""" | 
|  | password = self.cleaned_data.get('password') | 
|  | confirm_password = self.cleaned_data.get('confirm_password') | 
|  | if password != confirm_password: | 
|  | self.add_error('confirm_password', '两次密码不一致') | 
|  |  | 
|  | return self.cleaned_data | 
|  |  | 
注册视图类相关代码
项目使用模块
|  | from django.shortcuts import render, HttpResponse, redirect, reverse | 
|  | from django.http import JsonResponse | 
|  |  | 
|  |  | 
|  | from django.views import View | 
|  | from BBS.myforms.register import Register_from | 
|  |  | 
|  | from django.contrib import auth | 
|  | from django.contrib.auth.decorators import login_required | 
|  | from django.views.decorators.csrf import csrf_exempt, csrf_protect | 
|  | from django.utils.decorators import method_decorator | 
|  | from django.db.models import Q, F | 
后端代码
|  | class Register_class(View): | 
|  | """用户注册视图类""" | 
|  |  | 
|  | def get(self, request): | 
|  | form_obj = Register_from() | 
|  | return render(request, 'bbs/registerPage.html', locals()) | 
|  |  | 
|  | def post(self, request): | 
|  | re_dict = { | 
|  | 'code': 10000, | 
|  | 'msg': '' | 
|  | } | 
|  | print(request.POST, request.FILES) | 
|  | form_obj = Register_from(request.POST) | 
|  | if form_obj.is_valid(): | 
|  | clean_data = form_obj.cleaned_data | 
|  | clean_data.pop('confirm_password') | 
|  | file_obj = request.FILES.get('avatar') | 
|  | if file_obj: | 
|  | clean_data['avatar'] = file_obj | 
|  | models.UserInfo.objects.create_user(**clean_data) | 
|  | re_dict['url'] = reverse('login_url') | 
|  | else: | 
|  | re_dict['code'] = 10001 | 
|  | re_dict['msg'] = form_obj.errors | 
|  | return JsonResponse(re_dict) | 
html代码
|  | <!DOCTYPE html> | 
|  | <html lang="en"> | 
|  | <head> | 
|  | <meta charset="UTF-8"> | 
|  | <title>Title</title> | 
|  | <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script> | 
|  | {% load static %} | 
|  | <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="row"> | 
|  | <div class="col-md-6 col-md-offset-3"> | 
|  | <h1 class="text-center h1">用户注册</h1> | 
|  | <form id="form"> | 
|  | {% csrf_token %} | 
|  | {% for form in form_obj %} | 
|  | <div class="form-group"> | 
|  | <label for="{{ form.auto_id }}">{{ form.label }}</label> | 
|  | {{ form }} | 
|  | <span class="pull-right" style="color: red"></span> | 
|  | </div> | 
|  | {% endfor %} | 
|  | <div class="form-group"> | 
|  | <label for="myfile">头像 <img src="{% static 'img/default.png' %}" alt="" id="myimg" | 
|  | width="100"></label> | 
|  | <input type="file" id="myfile" style="display: none"> | 
|  | </div> | 
|  | <input type="button" id="mybtn" class="btn btn-block btn-success" value="注册"> | 
|  | </form> | 
|  | </div> | 
|  | </div> | 
|  | </div> | 
|  | <script> | 
|  | $('#myfile').change(function () { | 
|  | let redfile_obj = new FileReader() | 
|  | redfile_obj.readAsDataURL(this.files[0]) | 
|  | redfile_obj.onload = function () { | 
|  | $('#myimg').attr('src', redfile_obj.result) | 
|  | } | 
|  | }) | 
|  | $('#mybtn').click(function () { | 
|  | let formdata_obj = new FormData() | 
|  |  | 
|  | let file_obj = $('#myfile')[0].files[0] | 
|  | formdata_obj.append('avatar', file_obj) | 
|  | $.each($('#form').serializeArray(), function (index, data_obj) { | 
|  | formdata_obj.append(data_obj.name, data_obj.value) | 
|  | }) | 
|  | console.log(formdata_obj) | 
|  |  | 
|  | $.ajax({ | 
|  | url: '', | 
|  | type: 'post', | 
|  | data: formdata_obj, | 
|  |  | 
|  | //取消属性与数据 | 
|  | contentType: false, | 
|  | processData: false, | 
|  | success: function (arg) { | 
|  | if (arg.code === 10000) { | 
|  | window.location.href = arg.url | 
|  | } else { | 
|  | $.each(arg.msg, function (name, value) { | 
|  | let id_name = '#id_' + name | 
|  | $(id_name).next().text(value).parent().addClass('has-error') | 
|  | }) | 
|  | } | 
|  | } | 
|  | }) | 
|  |  | 
|  |  | 
|  | }) | 
|  | $('input').click(function () { | 
|  | $(this).next().text('').parent().removeClass('has-error') | 
|  | }) | 
|  |  | 
|  | </script> | 
|  |  | 
|  |  | 
|  | </body> | 
|  | </html> | 
登录功能
后端代码
|  | def login_func(request): | 
|  | """用户登录功能""" | 
|  | if request.method == 'POST': | 
|  | re_dict = { | 
|  | 'code': 10000, | 
|  | 'msg': '' | 
|  | } | 
|  | captcha: str = request.POST.get('captcha') | 
|  | username = request.POST.get('username') | 
|  | password = request.POST.get('password') | 
|  | if request.session.get('captcha').lower() == captcha.lower(): | 
|  | user_obj = auth.authenticate(request, username=username, password=password) | 
|  | if user_obj: | 
|  | auth.login(request, user_obj) | 
|  | re_dict['url'] = reverse('home_url') | 
|  | else: | 
|  | re_dict['code'] = 10001 | 
|  | re_dict['msg'] = { | 
|  | 'password': '用户名或密码错误' | 
|  | } | 
|  | else: | 
|  | re_dict['code'] = 10002 | 
|  | re_dict['msg'] = { | 
|  | 'captcha': '验证码错误', | 
|  | } | 
|  | if not username: | 
|  | re_dict['msg']['username'] = '用户名不能为空' | 
|  | if not password: | 
|  | re_dict['msg']['password'] = '密码不能为空' | 
|  | return JsonResponse(re_dict) | 
|  | return render(request, 'bbs/loginPage.html', locals()) | 
前端代码
|  | <!DOCTYPE html> | 
|  | <html lang="en"> | 
|  | <head> | 
|  | <meta charset="UTF-8"> | 
|  | <title>Title</title> | 
|  | <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script> | 
|  | {% load static %} | 
|  | <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="row"> | 
|  | <div class="col-md-6 col-md-offset-3"> | 
|  | <h1 class="text-center h1">用户登录</h1> | 
|  | <form id="form"> | 
|  | {% csrf_token %} | 
|  | <div class="form-group"> | 
|  | <label for="username">用户名</label> | 
|  | <input type="text" name="username" id="username" class="form-control"> | 
|  | <span class="pull-right" style="color: red"></span> | 
|  | </div> | 
|  | <div class="form-group"> | 
|  | <label for="password">密码</label> | 
|  | <input type="text" name="password" id="password" class="form-control"> | 
|  | <span class="pull-right" style="color: red"></span> | 
|  | </div> | 
|  | <div class="form-group"> | 
|  | <label for="captcha">验证码 </label> | 
|  | <div class="row"> | 
|  | <div class="col-md-6"> | 
|  | <input type="text" name="captcha" class="form-control" id="captcha"> | 
|  | <span class="pull-right" style="color: red"></span> | 
|  | </div> | 
|  | <div class="col-md-6" id="div_captcha" onclick="$('#captcha_img').attr('src','{% url 'captcha_url' %}?'+Math.random())"><img src="{% url 'captcha_url' %}" alt="" width="260" height="35" id="captcha_img"> | 
|  | <p class="pull-right btn-link"><a href="javascript:void(0);" | 
|  | id="change">看不清?点击换一张</a></p> | 
|  | </div> | 
|  |  | 
|  | </div> | 
|  | </div> | 
|  | <input type="button" id="loginBtn" class="btn btn-block btn-success" value="登录"> | 
|  | </form> | 
|  | </div> | 
|  | </div> | 
|  | </div> | 
|  | <script> | 
|  | $('#loginBtn').click(function () { | 
|  | $.ajax({ | 
|  | url: '', | 
|  | type: 'post', | 
|  | dataType: 'json', | 
|  | data: $('#form').serializeArray(), | 
|  | success: function (args) { | 
|  | console.log(args) | 
|  | if (args.code === 10000) { | 
|  | window.location.href = args.url | 
|  | } else{ | 
|  | $('#div_captcha').click() | 
|  | $.each(args.msg,function (name,value) { | 
|  | let id_name = '#'+name | 
|  | $(id_name).next().text(value).parent().addClass('has-error') | 
|  | }) | 
|  | } | 
|  |  | 
|  |  | 
|  | } | 
|  | }) | 
|  | }) | 
|  | $('input').click(function () { | 
|  | $(this).next().text('').parent().removeClass('has-error') | 
|  | }) | 
|  | </script> | 
|  | </body> | 
|  | </html> | 
验证码功能代码
后端代码
|  | from PIL import Image, ImageFont, ImageDraw | 
|  | from io import BytesIO, StringIO | 
|  | import random | 
|  |  | 
|  |  | 
|  | def get_random_color(): | 
|  | """获取随机rgb颜色""" | 
|  | return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) | 
|  |  | 
|  |  | 
|  | def captcha_func(request): | 
|  | """生成验证码存储到session中,返回验证码图片""" | 
|  | imgobj = Image.new('RGB', (260, 35), get_random_color()) | 
|  | imgfont = ImageFont.truetype(r'static/font/云峰静龙行书.ttf', size=32) | 
|  | imgdraw = ImageDraw.ImageDraw(imgobj) | 
|  | code = '' | 
|  | for num in range(5): | 
|  | """剩余5位数验证吗""" | 
|  | choice_big = chr(random.randint(65, 90)) | 
|  | choice_small = chr(random.randint(97, 122)) | 
|  | choice_int = str(random.randint(0, 9)) | 
|  | choice_code = random.choice([choice_big, choice_small, choice_int]) | 
|  | imgdraw.text((num * 40 + 40, -3), choice_code, get_random_color(), imgfont) | 
|  | code += choice_code | 
|  | io_obj = BytesIO() | 
|  | imgobj.save(io_obj, 'png') | 
|  | request.session['captcha'] = code | 
|  | request.session.set_expiry(60 * 5) | 
|  | return HttpResponse(io_obj.getvalue()) | 
|  |  |