bbs项目(部分讲解)
项目开发基本流程
- 需求分析
- 架构设计
- 分组开发
- 提交测试
- 交付上线
项目分析流程
仿BBS项目:
- 仿造博客园项目
- 核心:文章的增删改查
- 技术栈
Django
、MySQL
- 功能
- 注册 (forms校验,页面渲染,上传头像)
- 登录 (自定义图片验证码)
- 首页:文章展示、侧边栏过滤(分类,标签,时间)
- 文章详情:点赞点踩、评论(父评论和子评论)
- 后台管理:当前用户文章展示(文章增删改查)
- 发布文章
- 项目版本信息:
python3.8
、django2.2.2
、mysql:5.7
、jquery2.x
、bootstrap3
项目表设计及关联
创建数据库bbs
create database bbs
表分析
一共需要创建七张表
- 用户表(基于
auth
模块的user
表扩写) - 个人站点表(跟用户表一对一关系)
- 分类表(和个人站点表一对多、和文章表一对多)
- 标签表(和个人站点表一对多、和文章表多对多)
- 点赞点踩表(和用户表一对多、和文章表一对多)
- 评论表(和用户表一对多,和文章表一对多)
- 文章表(和个人站点表一对多)
前期准备
创建项目
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) {
}
打印结果
现在后端可以收到数据 继续写后端
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')
}
})
}
}
})
打印结果
此时的错误提示信息不会消失 需要绑定一个定时任务
// 定时任务 渲染的错误信息三秒后清除
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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性