项目开发基本流程

  1. 需求分析
  2. 架构设计
  3. 分组开发
  4. 提交测试
  5. 交付上线

项目分析流程

仿BBS项目:

  • 仿造博客园项目
    • 核心:文章的增删改查
  • 技术栈 DjangoMySQL
  • 功能
    • 注册 (forms校验,页面渲染,上传头像)
    • 登录 (自定义图片验证码)
    • 首页:文章展示、侧边栏过滤(分类,标签,时间)
    • 文章详情:点赞点踩、评论(父评论和子评论)
    • 后台管理:当前用户文章展示(文章增删改查)
    • 发布文章
  • 项目版本信息:python3.8django2.2.2mysql:5.7jquery2.xbootstrap3

项目表设计及关联

创建数据库bbs

create database bbs

表分析

一共需要创建七张表

  1. 用户表(基于auth模块的user表扩写)
  2. 个人站点表(跟用户表一对一关系)
  3. 分类表(和个人站点表一对多、和文章表一对多)
  4. 标签表(和个人站点表一对多、和文章表多对多)
  5. 点赞点踩表(和用户表一对多、和文章表一对多)
  6. 评论表(和用户表一对多,和文章表一对多)
  7. 文章表(和个人站点表一对多)

image

前期准备

创建项目

1.安装django 2.2.2版本

pip3 install django==2.2.2

2.使用pycharm创建django项目

配置setting.py

TEMPLATES = {
"DIRS":[os.path.joi(BASE_DIR, "templates")]
}

配置语言环境

LANGUAGE = 'zh-hans'  # 语言汉化
TIME-ZONE = 'Asia/Shanghai'  # 时区使用上海时区
USE_I18N = True
USE_L10N = True
USE_TZ = False

配置数据库

DATABASES = {
	'default': {
		'ENGINE':'django.db.backends.mysql',
		'NAME': 'bbs',
		'HOST': '127.0.0.1',
		'PORT': 3306,
		'USER': 'root',
		'PASSWORD':'password'
	}
}

在models中写表模型

from django.db import models

# Create your models here.
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):  # 继承AbstractUser表 只用写auth表中没有的字段
    phone = models.CharField(max_length=32, null=True, verbose_name='用户手机号')
    # upload_to是文件保存在什么路径
    icon = models.FileField(upload_to='icon/', default='icon/default.png', null=True, verbose_name='用户头像')
    # 用户表和博客表一对一
    blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True)


class Blog(models.Model):
    title = models.CharField(max_length=32, null=True, verbose_name='主标题')
    site_title = models.CharField(max_length=32, null=True, verbose_name='副标题')
    site_style = models.CharField(max_length=64, null=True, verbose_name='站点样式')


class Tag(models.Model):
    name = models.CharField(max_length=32, verbose_name='标签名', null=True)
    # 标签和博客是一对多 一个博客有多个标签
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class Classify(models.Model):
    name = models.CharField(max_length=32, verbose_name='分类名')
    # 分类和博客是一对多关系 一个博客有多个分类
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class Article(models.Model):
    title = models.CharField(max_length=32, verbose_name='文章标题')
    desc = models.CharField(max_length=255, verbose_name='文章摘要')
    content = models.TextField(verbose_name='文章内容')
    create_time = models.DateTimeField(auto_now_add=True)  # 第一次创建时自动添加时间
    # 文章和分类表是一对多 一个分类有多篇文章
    classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE)
    # 文章和标签是多对多关系 自动创建第三张表
    tag = models.ManyToManyField(to='Tag')
    # 文章和博客是一对多关系 一个博客对应多篇文章
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class UpAndDown(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='点赞点踩时间')
    # 和用户表是一对多关系 一个用户可以有多条点赞点踩记录
    user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
    # 和文章也是一对多
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
    # 1代表点赞 0代表点踩
    is_up = models.BooleanField(verbose_name='是否点赞')


class Comment(models.Model):
    content = models.CharField(max_length=64, verbose_name='评论内容')
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
    user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
    # 自关联字段 只能存已有评论的主键值
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
    # 自关联的其他方式
    # parent = models.ForeignKey(to='Comment', on_delete=models.CASCADE)
    # parent = models.IntegerField(null=Ture)

终端执行数据库迁移命令

python38 manage.py makemigretions
python38 manage.py migrater

注册功能

注册forms编写

在根目录下创建blog_forms.py文件

from django import forms
from django.forms import widgets
from blog.models import UserInfo
from django.core.exceptions import ValidationError  # 合法性错误


class User(forms.Form):
    # 用户名 密码 确认密码 邮箱
    username = forms.CharField(max_length=8, min_length=3, label='用户名', required=True,
                               error_messages={'max_length': '用户名最多只能输入8位',
                                               'min_length': '用户名最少输入3位',
                                               'required': '用户名必须填'
                                               },
                               # 添加bootstr样式
                               widget=widgets.TextInput(attrs={'class': 'form-control'})
                               )
    password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                               error_messages={
                                   'max_length': '密码最长16位',
                                   'min_length': '密码最短8位',
                                   'required': '密码不能为空',
                               },
                               widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                               )
    re_password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                                  error_messages={
                                      'max_length': '密码最长16位',
                                      'min_length': '密码最短8位',
                                      'required': '密码不能为空',
                                  },
                                  widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                                  )
    email = forms.EmailField(label='邮箱地址', widget=widgets.EmailInput(attrs={'class': 'form-control'}))

    # 局部钩子 校验用户名是否存在
    def clean_username(self):
        name = self.cleaned_data.get('username')
        if UserInfo.objects.filter(username=name).first():
            # 用户已存在
            raise ValidationError('用户名已存在')  # 校验错误抛出异常
        else:
            return name

    # 局部钩子 校验用户名是否存在
    # def clean_username(self):
    #     username = self.cleaned_data.get('username')
    #     try:
    #         UserInfo.objects.get(username=username)
    #         print(UserInfo.objects.get(username=username), type(UserInfo.objects.get(username=username)))
    #         raise ValidationError('用户名已存在')
    #     except Exception:
    #         return username

    # 全局钩子 校验两次输入密码是否一致
    def clean(self):
        pwd = self.cleaned_data.get('password')
        re_pwd = self.cleaned_data.get('re_password')
        if pwd != re_pwd:
            raise ValidationError('两次密码不一致')  # 主动抛出合法性错误
        else:
            return self.cleaned_data

路由配置

在项目同名文件夹下的urls.py中配置路由

from django.contrib import admin
from django.urls import path
from blog import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', views.register),
]

编写视图函数

views.py

from django.shortcuts import render
from blog.blog_forms import User


def register(request):
    form_obj = User()
    if request.method == 'GET':  # 当请求为get时返回注册界面,并返回forms组件对象进行数据校验
        return render(request, 'register.html', {'form_obj': form_obj})

前端模板编写

register.html

需要先配置静态文件
	-在setting.py中
		STATICFILES_DIRS = [
			os.path.join(BASE_DIR, 'static')
	] 
	-把bootstrap和jquery导入模板中
<body>
	<div class="container-fluid">
	    <div class="row">
	        <div class="col-md-6 col-md-offset-3">
	            <h1 class="text-center text-info">注册功能</h1>
	            <form action="" id="id_form">
	                {% csrf_token %}
	                {% for foo in form_obj %}
	                    <div class="form-group">
	                        <label for="{{ foo.id_for_label }}">{{ foo.label }}</label>
	                        {{ foo }}
	                        <span class="pull-right text-danger"></span>
	                    </div>
	                {% endfor %}
	                <div class="form-group">
	                    <label for="id_file">头像
	                        <img src="/static/default.png" alt="" height="100px" width="100px" style="margin-right: 20px"
	                             id="id_img">
	                        <input type="file" id="id_file" accept="image/*" style="display: none">
	                    </label>
	                </div>
	                <div class="form-group text-center">
	                    <!--提交按钮不能是submit或者单独的button按钮 如果写了ajax 点击提交 就会发送两次请求-->
	                    <input type="button" value="注册" class="btn btn-success" id="id_submit">
	                    <span class="text-danger error"></span>
	                </div>
	            </form>
	        </div>
	    </div>
	</div>
</body>

头像动态显示

<script>
    // 头像动态显示
    $('#id_file').change(function () {
        // 将上传的头像展示到img标签内 修改img标签内的src参数

        // 读出图片文件 借助于文件阅读器
        let reader = new FileReader()

        // 拿到文件对象
        let file = $('#id_file')[0].files[0]

        // 将文件对象读到文件阅读器中
        reader.readAsDataURL(file)

        // 文件加载完后修改img标签的src参数
        reader.onload = function () {
            // $('#id_img')[0].src=reader.result
            $('#id_img').attr('src', reader.result)  # jquery对象方法
        }
    })
</script>

发送ajax请求

// 发送ajax请求
    $('#id_submit').click(function () {
        let data = new FormData  // 可以传递文件数据

        // 方式一:根据id获取标签数据添加至data中
        
        // data.append('username', $('#id_username').val())
        // data.append('password', $('#id_password').val())
        // data.append('re_password', $('#id_re_password').val())
        // data.append('email', $('#id_email').val())
        // data.append('icon', $('#id_file')[0].files[0])
        // data.append('csrfmiddlewaretoken', $("[name='csrfmiddlewaretoken']").val())
        // ...发送ajax请求


        // 方式二:利用form组件批量处理
        let data_arr = $('#id_form').serializeArray()  // 序列化数组
        console.log(data_arr)  // 是一个数组套对象 对象中k是name v是value 自动添加csrf

        // 使用for循环把数据添加到data对象中
        $.each(data_arr, function (i, v) {
            console.log("index:",i)
            console.log("value:", v)
            console.log("-----------------------")
            data.append(v.name, v.value)
        })

        // 文件需要单独放入
        data.append('icon', $('#id_file')[0].files[0])

        // 使用ajax发送请求
        $.ajax({
            url: '/register/',
            type: 'post',
            data: data,
            processData: false,
            contentType: false,
            success: function (data) {

			}

image

打印结果

image

现在后端可以收到数据 继续写后端

views.py

def register(request):
    form_obj = User()
    if request.method == 'GET':
        return render(request, 'register.html', {'form_obj': form_obj})
    else:  # 当发送post请求
        res = {'code': 100, 'msg': '注册成功'}
        forms_obj = User(data=request.POST)  # forms组件检验
        if forms_obj.is_valid():  # 如果数据全部合法
            register_data = forms_obj.cleaned_data  # 拿出所有的合法数据
            register_data.pop('re_password')  # 弹出二次输入密码 因为用户表中不需要改字段
            if request.FILES.get('icon'):  # 判断是否上传了图片文件
                register_data['icon'] = request.FILES.get('icon')  # 上传了的话就添加进去
            # 一定要用create_user 密码是密文 后面才可以使用auth模块的功能
            UserInfo.objects.create_user(**register_data)  # 将register_data打散保存至数据库
            return JsonResponse(res)  # 注册成功返回信息
        else:  # 弱国数据不是全部合法
            res['code'] = 101
            res['msg'] = '注册失败'
            res['errors'] = forms_obj.errors  # 返回错误信息
            return JsonResponse(res)

前端ajax可以接受到后端返回的json字符串

$.ajax({
     url: '/register/',
     type: 'post',
     data: data,
     processData: false,
     contentType: false,
     success: function (data) {
         console.log(data)
         if (data.code === 100) {
             // 注册成功跳转至登录界面
             location.href = '/login/'
         } else {
             // 在前端渲染出错误信息
             console.log(data)
             $.each(data.errors, function (k, v) {// for循环错误字典
                 if (k === '__all__') {
                     // 全局钩子错误 两次密码不一致
                     $('.error').html(data.errors['__all__'][0])
                 } else {
                     // 其他错误找到相应的input框后的span标签渲染 父类标签加上has-error属性变红
                     $('#id_' + k).next().html(v[0]).parent().addClass('has-error')
                 }
             })
         }
     }
 })

打印结果

image

此时的错误提示信息不会消失 需要绑定一个定时任务

// 定时任务 渲染的错误信息三秒后清除
setTimeout(function () {
    // 把所有的span标签的内容清除 父类中的属性has-error去除
    $('.text-danger').html('').parent().removeClass('has-error')
}, 3000)

校验用户是否存在

需求:当用户输入用户名后鼠标离开用户名框,校验用户名是否存在且不能刷新页面

前端

<script>
	// 后端ajax校验用户名是否存在
    // 前端使用get请求传入用户名
    
    // 绑定一个失去焦点事件
    $('#id_username').blur(function () {
        $.ajax({
            url: '/check_name/?name=' + $('#id_username').val(),
            type: 'get',
            success: function (data) {
                if (data.code === 110) {// 当用户名存在 添加提示信息
                    $('#id_username').next().html(data.msg)
                }else {// 当用户不存在时清除提示信息
                    $('#id_username').next().html('')
                }
            }
        })
    })
</script>

后端

urls.py

path('check_name/', views.check_name),

views.py

def check_name(request):
    # print(request.GET)
    res = {'msg': '用户已存在', 'code': 110}
    name = request.GET.get('name')
    obj = UserInfo.objects.filter(username=name).first()
    if obj:
        return JsonResponse(res)
    else:
        res['code'] = 100
        res['msg'] = '用户不存在'
        return JsonResponse(res)

登录功能

登陆界面搭建

注册成功跳转至/login/,创建login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/jQuery.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-fluid">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <h1 class="text-center text-info">登录功能</h1>
            <form action="" id="id_form" method="post">
                {% csrf_token %}  <!--跨站请求伪造-->
                <div class="form-group">
                    <label for="id_username">用户名</label>
                    <input type="text" id="id_username" name="username" class="form-control">
                </div>
                <div class="form-group">
                    <label for="id_password">密码</label>
                    <input type="password" id="id_password" name="password" class="form-control">
                </div>
                <div class="row">
                    <div class="col-md-6 form-group">
                        <label for="id_code">验证码</label>
                        <input type="text" id="id_code" class="form-control" name="code">
                    </div>
                    <div class="col-md-6">
                        <img src="/get_code/" alt="" id="id_img" width="350px" height="50px">  <!--去后端获取随机验证码-->
                    </div>
                </div>
                <div class="form-group">
                    <input type="button" value="登录" class="btn btn-block btn-danger" id="id_submit">
                    <div class="text-center">
                        <span  class="text-danger error"></span>
                    </div>
                </div>
            </form>

            <script>
                // 点击验证码图片刷新验证码
                $('#id_img').click(function () {
                    let time = new Date().getTime()
                    console.log(time)
                    // 再次获取随机验证码图片
                    $('#id_img')[0].src = '/get_code/?t=' + time
                })

                // 提交ajax
                $('#id_submit').click(function () {
                	// 将form表单的input标签数据序列化成数组套对象 name value
                    dataArray = $('#id_form').serializeArray()
                    $.ajax({
                        url: '/login/',
                        type: 'post',
                        data: dataArray,
                        success: function (data) {
                            console.log(data)
                            if(data.code===100){
                                location.href = '/'
                            }else {
                                $('.error').html(data.msg)
                            }
                        }
                    })
                })


                // 定时器任务 自动关闭错误提示信息
                let test = function () {
                    $('.error').html('')
                }
                // 可重复关闭
                timer = setInterval(test, 2000)

				//60秒后关闭循环定时任务
                setTimeout(function () {
                    clearTimeout(timer)
                },60*1000)
            </script>
        </div>
    </div>
</div>
</body>
</html>

自定义图片验证码

验证码:字母数字共五位

views.py

from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
import random

def get_code(request):
	# 1 生成一张图片 pillow模块
	img = Image.new('RGB', (350, 50), color=(255, 255, 255))
	# 2 生成一个画图对象 将img传入
	draw = ImageDraw.Draw(img)
	# 3 生成字体对象
	font = ImageFont.truetype(font='./static/font/1641263938811335.ttf', size=50)
	# 4 生成随机字符串
	ran_str = ''
	for i in range(5):
		ran_num = str(random.randint(0, 9))
		ran_upper = chr(random.randint(65,90))
		# 去除I和L
		while (ran_upper == 'L' or ran_upper == 'I'):
			ran_upper = chr(random.randint(65,90))
		ran_lower = chr(random.randint(97, 122))
		# 去除i和l
		while (ran_lower == i or ran_lower == l):
			ran_lower = chr(random.randint(97, 122))
		res = random.choice([ran_num , ran_upper, ran_lower])
		# 将生成的随机字符画到图片中
		# fill=get_color 字体颜色也随机
		draw.text(xy=(10 + i * 60, 0), text=res, font=font, fill=get_color())
	# 5 画线
	    for i in range(10):
        draw.line([(random.randint(0, 350), random.randint(0, 50)), (random.randint(0, 350), random.randint(0, 50))],
                  fill=get_color())  # 起点和终点
    # 6 画点
    for i in range(100):
        draw.point((random.randint(0, 350), random.randint(0, 50)), fill=get_color())
	# 7 将图片保存在内存中 BytesIo模块 并返回给前端
	byte_io = BytesIo()
	img.save(fp=byte_io, format='png')
	# 怎样校验前端传过来的验证码?
	# 可以存在session表中 前端访问返回给前端 前端再次访问携带session 后端取出data进行校验
	request.session['code'] = res
	return HttpResponse(byte_io.getvalue())
	


def get_color():
	x, y = 0, 255	
	return (random.randint(x, y), random.randint(x, y), random.randint(x, y))

上面是自定义的图片验证码,也可以使用第三方模块,比如gvcode模块

from gvcode import VFCode

"""
使用方法:
vc = VFCode(
        width=200,                       # 图片宽度
        height=80,                       # 图片高度
        fontsize=50,                     # 字体尺寸
        font_color_values=[
            '#ffffff',
            '#000000',
            '#3e3e3e',
            '#ff1107',
            '#1bff46',
            '#ffbf13',
            '#235aff'
        ],                                # 字体颜色值
        font_background_value='#ffffff',  # 背景颜色值
        draw_dots=False,                  # 是否画干扰点
        dots_width=1,                     # 干扰点宽度
        draw_lines=True,                  # 是否画干扰线
        lines_width=3,                    # 干扰线宽度
        mask=False,                       # 是否使用磨砂效果
        font='arial.ttf'                  # 字体 内置可选字体 arial.ttf calibri.ttf simsun.ttc
    )
    # 验证码类型
    # 自定义验证码
    # vc.generate('abcd')

    # 数字验证码(默认5位)
    # vc.generate_digit()
    # vc.generate_digit(4)

    # 字母验证码(默认5位)
    # vc.generate_alpha()
    # vc.generate_alpha(5)

    # 数字字母混合验证码(默认5位)
    # vc.generate_mix()
    # vc.generate_mix(6)

    # 数字加减验证码(默认加法)
    vc.generate_op()
    # 数字加减验证码(加法)
    # vc.generate_op('+')
    # 数字加减验证码(减法)
    # vc.generate_op('-')

    # 图片字节码
    # print(vc.get_img_bytes())
    # 图片base64编码
    print(vc.get_img_base64())
    # 保存图片
    vc.save()
"""
def get_code(request):
    vc = VFCode(width=350, height=50)
    vc.generate_mix()
    # vc.generate_op()
    print(vc.get_img_base64()[0])
    byte_io = BytesIO()
    vc.save(byte_io, fm='png')
    request.session['code'] = vc.get_img_base64()[0]
    return HttpResponse(byte_io.getvalue())

登陆界面前端发送数据

login.html

<script>

// 提交ajax
$('#id_submit').click(function () {
     let dataArray = $('#id_form').serializeArray()
     console.log(dataArray)
     $.ajax({
         url: '/login/',
         type: 'post',
         data: dataArray,
         success: function (data) {
             console.log(data)
             if(data.code===100){
             	 // 登陆成功 去首页
                 location.href = '/'
             }else {
             	 // 登陆失败 显示错误信息
                 $('.error').html(data.msg)
             }
         }
     })
 })

// 计时器 关闭错误提示
let test = function () {
	$('.error').html('')
}
// 循环执行
timer = setInterval(test, 2000)

//60秒后关闭循环定时任务
setTimeout(function () {
	clearTimeout(timer)
},60*1000)
														            
</script>

登录后端

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    res = {'code': 100, 'msg': '登陆成功'}
    code = request.POST.get('code')
    # 校验验证码
    if request.session.get('code').lower() == code.lower():
        username = request.POST.get('username')
        password = request.POST.get('password')
        
        # 如果认证成功(用户名和密码正确有效),便会返回一个 User 对象。
        obj = authenticate(username=username, password=password)
        if obj:
            return JsonResponse(res)
        res['code'] = 110
        res['msg'] = '用户名或密码错误'
        return JsonResponse(res)
    res['code'] = '120'
    res['msg'] = '验证码错误'
    return JsonResponse(res)