赞
踩
源码
首页
不同板块
注册页面
登录页面
个人信息
写文章页面
文章详情页面
文章评论
本文主要讲解 Python
后端部分,由于仅仅用到了 vue
作为 js
框架并非前后端分离项目,故前端不单独介绍。
本项目实现了一个网页端的 博客系统 ,该博客系统允许多人注册登录,用户可以在网站上面发布博客,浏览别人发布的博客。
实际意义在于:当一个小组或者一个班级需要进行学习与交流的时候可以用到,大家都可以在上面分享自己的学习心得,然后互相学习,由于本项目分了板块,所以要查找相关的技术栈也很方便。
需求驱动开发,先分析需求(这里参考了网上其它博客系统的需求):
富文本
编辑器,用户使用图形化界面即可写出 HTML
代码存储在数据库中;Django
+ Django
模板引擎;Python 3.6
MySQL 5.7
Django 3.0
Redis 3.2
主要涉及四个实体:
他们之间的 关系 如下:
有了实体和关系,下面用 ER
图表示一下:
加上 属性(属性不全,在概念模型中会补全):
然后设计它的 概念模型 :
对应的 物理模型 为:
在物理模型中,由于存在一对多的关系,所以文章表和评论表中加上了两个 外键约束 。
一般到这里可以直接创建数据库和表了,但是由于使用的是 Django,他集成了 ORM
框架,即 Object Relation Mapping
对象关系映射,所以我们不必直接写 SQL
语句,而是可以写出实体类,再执行 Django
的文件迁移命令,就可以自动生成数据表了。
先来看 User
实体:
class User(AbstractUser):
# 手机号(长度为11, 唯一, 不为空)
mobile = models.CharField(max_length=11, unique=True, blank=False)
# 头像信息(图片类型的, 保存到项目目录下的 avatar 文件夹下_以日期创建文件夹区分, 可以为空)
avatar = models.ImageField(upload_to='avatar/%Y%m%d/', blank=True)
# 简介信息(最大长度为500, 可以为空)
user_desc = models.CharField(max_length=500, blank=True)
这里之定义了三个字段,为什么?
因为我们集成了 Django
自带的一个实体类 AbstractUser
,这个是实体类为我们提供了用户名、姓名、邮箱地址等通用信息,所以我们就不用单独指定了。
但是要在配置文件中说明自己已经更改了用户类:
# 替换系统的用户模型为我们自定义的用户模型
AUTH_USER_MODEL = 'users.User'
自动生成的 users
表:
文章分类
实体:
class ArticleCategory(models.Model):
"""
文章分类实体类
"""
# 栏目标题
title = models.CharField(max_length=100, blank=True)
# 创建时间
created = models.DateTimeField(default=timezone.now)
文章
实体类:
class Article(models.Model): """ 文章实体类 """ # 外键约束: 和用户表关联在一起: 设置级联删除: 即删除用户的同时会删除该用户的所有文章 author = models.ForeignKey(User, on_delete=models.CASCADE) # 文章标题图: 图片类型的, 保存到项目目录下的 article 文件夹下_以日期创建文件夹区分, 可以为空 avatar = models.ImageField(upload_to='article/%Y%m%d/', blank=True) # 文章栏目是 "一对多" 外键: 一个栏目对应多个文章 category = models.ForeignKey( ArticleCategory, null=True, blank=True, on_delete=models.CASCADE, related_name='article' ) # 文章标签 tags = models.CharField(max_length=20, blank=True) # 文章标题 title = models.CharField(max_length=100, null=False, blank=False) # 概要 summary = models.CharField(max_length=200, null=False, blank=False) # 文章正文 content = models.TextField() # 浏览量: 正整数 total_view = models.PositiveIntegerField(default=0) # 文章评论数 comments_count = models.PositiveIntegerField(default=0) # 文章创建时间: 默认写当前的时间 created = models.DateTimeField(default=timezone.now) # 文章更新时间: 自动写入当前时间 updated = models.DateTimeField(auto_now=True)
评论
实体类:
class Comment(models.Model):
"""
评论实体类
"""
# 评论内容
content = models.TextField()
# 评论的文章
article = models.ForeignKey(Article, on_delete=models.SET_NULL, null=True)
# 发表评论的用户
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True)
# 评论时间
created = models.DateTimeField(auto_now_add=True)
项目结构如下:
home
为子应用:管理博客和评论libs
为依赖的第三方库:图片验证码和手机号短信验证码;logs
没传上来:用于日志输出;media
媒体资源文件:头像图片之类的;my_blog
主应用:用于注册其它应用,设置超级管理员等;static
静态资源目录:js 和 css 等;template
模板引擎文件夹:就是几个主页面;users
用户子应用:用于实现用户登陆等功能;utils
工具类包:自定义装填码信息;路由结构如下:
根路由:
urlpatterns = [
path('admin/', admin.site.urls),
# include 首先设置一个元组(子应用的路由, 子应用的名字)
# 然后设置命名空间 namespace 可以防止不同应用路由重名的问题
path('', include(('users.urls', 'users'), namespace='users')),
path('', include(('home.urls', 'home'), namespace='home'))
]
users 子路由:
urlpatterns = [ # path(路由, 视图函数名) path('register/', RegisterView.as_view(), name='register'), # 图片验证码的路由 path('imagecode/', CaptchaView.as_view(), name='imagecode'), # 手机验证码的路由 path('smscode/', SmsCodeView.as_view(), name='smscode'), # 登陆的路由 path('login/', LoginView.as_view(), name='login'), # 退出登陆 path('logout/', LogoutView.as_view(), name='logout'), # 忘记密码 path('forgetpassword/', ForgetPasswordView.as_view(), name='forgetpassword'), # 用户个人信息页面 path('center/', UserCenterView.as_view(), name='center'), # 写文章的路由 path('writeblog/', WriteBlogView.as_view(), name='writeblog'), ]
home 子路由:
urlpatterns = [
# 首页的路由 即什么路径都不写就会跳转到首页
path('', IndexView.as_view(), name='index'),
path('detail/', DetailView.as_view(), name='detail'),
]
这里使用了路由的 namespace
设置路由的命名空间,主要是为了后面引用方便。
比如在页面种经常这么写:
Vue
作为前端框架;Django
作为后端框架;Django
模板引擎;云通讯
短信发送;session
技术;这里我将注册部分做的过于复杂,按理说我这种小网站不需要手机短信验证码啥的,我这里这么做的原因主要是学习使用短信验证码进行认证,因为之前在开发的时候没有用到过短信验证码,用过邮箱验证码,这次在本项目中用到了之后再做相似的项目就比较熟悉了。
值得一提的是使用了 Django
自带的日志记录功能,在 settings
文件中配置如下:
# 设置日志 LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 是否禁用已经存在的日志器 'formatters': { # 日志信息显示的格式 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, 'simple': { 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' }, }, 'filters': { # 对日志进行过滤 'require_debug_true': { # django在debug模式下才输出日志 '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { # 日志处理方法 'console': { # 向终端中输出日志 'level': 'INFO', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { # 向文件中输出日志,日志会以文件的方式保存到项目目录下的 logs 文件夹下面; 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(BASE_DIR, 'logs/blog.log'), # 日志文件的位置 'maxBytes': 300 * 1024 * 1024, 'backupCount': 10, 'formatter': 'verbose' }, }, 'loggers': { # 日志器 'django': { # 定义了一个名为django的日志器 'handlers': ['console', 'file'], # 可以同时向终端与文件中输出日志 'propagate': True, # 是否继续传递日志信息 'level': 'INFO', # 日志器接收的最低日志级别为 INFO }, } }
然后就可以在 logs/blog.log
文件下查看日志信息了:
通过 Django 自带的 后台管理系统 管理后台:
创建超级用户
17858918831
wangshuo
wangsuoo@qq.com
wsuo2821
通过蠕虫复制更多的数据:
INSERT INTO tb_article(avatar,tags,title,summary,content,total_view,comments_count,created,updated,author_id,category_id)
SELECT avatar,tags,title,summary,content,total_view,comments_count,created,updated,author_id,category_id FROM tb_article;
这里使用了一个第三方库实现了功能,工具地址:https://bitbucket.org/akorn/wheezy.captcha
前端用户可以点击切换验证码,这里使用的策略是,写一个接口用于返回验证码图片,为了实现定时过期的功能,我存到了 Redis 中,这样指定时间之后验证码就会过期:
""" 生成验证码并且存储到 Redis 中 :param request: 请求对象 :return: 返回值,这里是一个响应对象 """ # 首先从前端获取到验证码的 uuid uuid = request.GET.get('uuid') # 然后做一个判断,确保 UUID 存在 if uuid is None: return HttpResponseBadRequest("验证码有误!") # 通过验证码库生成验证码 text, image = captcha.generate_captcha() # 获取 Redis 连接 redis_conn = get_redis_connection("default") # 存入 Redis 中, (键, 存活时间, 值) 300s = 5分钟 redis_conn.setex("img:%s" % uuid, 300, text) # 返回给前端验证码图片 return HttpResponse(image, content_type="image/jpeg")
将验证码的 UUID
存储到 Redis
作为 key
,将真正的验证码的值作为 value
存储进去,这样之后校验的时候就可以直接拿过来比较,如果匹配则可以注册,不匹配则报错。
如图,Redis
中存储的值:
这里使用的是 容联云通讯
网址:https://www.yuntongxun.com/
注册之后可以将自己的电话号码设置为测试账号,仅可以向自己的手机号发短信,测试成功的页面如下:
使用的是官方提供的接口,在项目文件中将自己的密钥信息填进去就能用:
可以使用 main
函数测试:
接口设计:
""" 用 Random 库生成随机的手机验证码,然后存储到 Redis 中,同时在控制台打印输出,方便调试 最后调用 '容联云' 的接口发送验证码: 注意目前这里只能向我指定的手机号发送验证码. 17858918830 """ # 随机生成 6 位验证码 sms_code = '%06d' % randint(0, 999999) # 将验证码输出到控制台 logging.info(sms_code) # 保存到 Redis 中 redis_conn.setex('sms:%s' % mobile, 300, sms_code) # 发送短信 17858918830; # 参数一是手机号; # 参数二是模板中的内容, 您的验证码为 1234, 请于 5 分钟之内填写; # 参数三是短信模板,用于测试只能是1. CCP().send_template_sms(mobile, [sms_code, 5], 1) # 响应结果 return JsonResponse({'code': RETCODE.OK, 'errmsg': '发送短信成功'})
这里采取的策略是,先使用 random
库生成一个 6 位的随机数,然后将验证码保存到 Redis
中,并设置过期时间,这里为了方便调试也把它打印到 控制台 了,实际测试中我的手机确实可以接收到验证码,也注册成功了,就是官方的这个免费的接口响应比较慢。
状态保持:
将通过认证的用户的唯一标识信息(比如:用户ID)写入到当前 session
会话中;
Django 用户认证系统提供了 login
() 方法封装了写入 session
的操作,帮助我们快速实现状态保持;
login()
位置:django.contrib.auth.__init__.py
文件中。
同时如果用户点击了记住我按钮之后,就会将登录信息写入 cookie
中。
# 认证登陆的用户: authenticate 是 Django 自带的认证方法 user = authenticate(mobile=mobile, password=password) if user is None: return HttpResponseBadRequest('用户名或者密码错误') # 实现登陆状态保持 login(request, user) # 根据 next 参数来进行页面的跳转: 由用户信息页面传递的参数 nex = request.GET.get('next') if nex: response = redirect(nex) else: response = redirect(reverse('home:index')) # 存储到 Cookie 中 if remember != 'on': # 用户没有选择记住我,浏览区会话结束就过期 request.session.set_expiry(0) response.set_cookie('is_login', True) response.set_cookie('username', user.username, max_age=30 * 24 * 3600) else: # 用户选择了记住我: set_expiry 默认两周之后过期 request.session.set_expiry(None) response.set_cookie('is_login', True, max_age=14 * 24 * 3600) response.set_cookie('username', user.username, max_age=30 * 24 * 3600)
而且这里由于配置了 Redis 的名称为 session
,所以会自动存储到 Redis 中,持续时间为一个月,也就是说关掉浏览器之后一个月之内都不用重新登陆。
"session": { # session
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
# 将 session 的存取由数据库存储改为 Redis 存储
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"
我前两天测试的,现在 Redis
中还有:
查看使用的 Cookie 信息:
# 默认的认证方法中是对username进行认证。我们需要修改认证的字段为mobile。
# 所以我们需要在User的模型中修改
USERNAME_FIELD = 'mobile'
可以看到电话号码已经作为 username 存储到 cookie 中了
使用模板语言:
<a class="dropdown-item" href='{% url "user:logout" %}'>退出登录</a>
已经显示登陆了:
这个时候点击退出登录发现已经没有了:
图片属于静态资源,在说图片上传之前先说一下怎么访问的静态资源,我是在根目录下创建了一个 static
文件夹,然后在 settings
中配置如下信息:
# 访问静态资源的路由
STATIC_URL = '/static/'
# 设置本地静态资源的加载路径
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
图片上传经过 Django 的封装之后就变得非常简单了,我在定义用户实体的时候就指定了上传文件夹:
# 头像信息(图片类型的, 保存到项目目录下的 avatar 文件夹下_以日期创建文件夹区分, 可以为空)
avatar = models.ImageField(upload_to='avatar/%Y%m%d/', blank=True)
但是仅仅是这样还是不行的,要指定访问路径和访问路由。
# 设置图片上传路径
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
# 图片的统一路由
MEDIA_URL = '/media/'
# 设置图片路由访问规则
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
修改之后:
什么都没写错,但就是访问不到,报错信息如下:
看到 CSRF
就想起来了跨域问题,因为之前做 Web
开发的时候也遇到过类似的情况,可以在前端解决,也可以在后端解决,这里 Django 提供了一种更简单的方法:
直接在表单下面添加一个标签即可。
Django自带的类:
让我们的类实现该类:
然后启动:
报错找不到,因为 Django 默认的跳转连接是 accounts
,需要在设置里修改:
# 设置未登录用户跳转的路由
LOGIN_URL = '/login/'
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。