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()) |
| |