页面展示:

源码

首页

不同板块

注册页面

登录页面

个人信息

写文章页面

文章详情页面

文章评论

Pythonvuejs

一、项目内容(做什么)

本项目实现了一个网页端的 博客系统 ,该博客系统允许多人注册登录,用户可以在网站上面发布博客,浏览别人发布的博客。

实际意义在于:当一个小组或者一个班级需要进行学习与交流的时候可以用到,大家都可以在上面分享自己的学习心得,然后互相学习,由于本项目分了板块,所以要查找相关的技术栈也很方便。

项目主要模块

需求驱动开发,先分析需求(这里参考了网上其它博客系统的需求):

本项目实现的主要功能:

富文本HTML

二、项目方案(怎么做)

项目开发模式

DjangoDjango

语言及工具版本

Python 3.6MySQL 5.7Django 3.0Redis 3.2

数据库设计

主要涉及四个实体:

  • 文章:用户:评论:分类

他们之间的 关系 如下:

  • 一个文章对应一个分类,而一个分类可以有多篇文章,所以他们之间的关系是多对一的;
  • 一个用户可以发布多篇文章,同时可以发表多个评论,而每一个评论只对应于一个用户,每一篇文章只属于一个用户,所以用户与文章、评论之间是一对多的;
ER

加上 属性(属性不全,在概念模型中会补全):

然后设计它的 概念模型

对应的 物理模型 为:

在物理模型中,由于存在一对多的关系,所以文章表和评论表中加上了两个 外键约束

建立数据表

ORMObject Relation MappingSQLDjango
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)

这里之定义了三个字段,为什么?

DjangoAbstractUser

但是要在配置文件中说明自己已经更改了用户类:

# 替换系统的用户模型为我们自定义的用户模型
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 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)

项目结构与路由设置

项目结构如下:

homelibslogsmediamy_blogstatictemplateusersutils

路由结构如下:

根路由:

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

比如在页面种经常这么写:

三、技术要点(关键技术)

VueDjangoDjango云通讯session

这里我将注册部分做的过于复杂,按理说我这种小网站不需要手机短信验证码啥的,我这里这么做的原因主要是学习使用短信验证码进行认证,因为之前在开发的时候没有用到过短信验证码,用过邮箱验证码,这次在本项目中用到了之后再做相似的项目就比较熟悉了。

日志管理

Djangosettings
# 设置日志
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;

四、难点及解决过程(所有碰到的问题、原因及解决方法)

1、图片验证码

这里使用了一个第三方库实现了功能,工具地址: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")
UUIDRediskeyvalue
Redis

2、手机短信验证码

容联云通讯

注册之后可以将自己的电话号码设置为测试账号,仅可以向自己的手机号发短信,测试成功的页面如下:

使用的是官方提供的接口,在项目文件中将自己的密钥信息填进去就能用:

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': '发送短信成功'})
randomRedis

3、登陆状态保持

状态保持:

sessionloginsessionlogin()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)
session
"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 中了

4、退出登陆

使用模板语言:

<a class="dropdown-item" href='{% url "user:logout" %}'>退出登录</a>

已经显示登陆了:

这个时候点击退出登录发现已经没有了:

5、图片上传

staticsettings
# 访问静态资源的路由
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)

修改之后:

6、跨域问题

什么都没写错,但就是访问不到,报错信息如下:

CSRFWeb

直接在表单下面添加一个标签即可。

7、用户访问问题

Django自带的类:

让我们的类实现该类:

然后启动:

accounts
# 设置未登录用户跳转的路由
LOGIN_URL = '/login/'
Star