报障系统

1. 需求分析

1.1 报障功能

1)用户本身

  • 提交报障单
  • 查看主机的报障记录

2)处理者

  • 查看所有人的报障单
  • 处理报障单

1.2 博客(知识库)

1)知识库主页

  • 展示最新的文章
  • 展示最热的文章
  • 展示评论最多的文章
  • 分类查看

2)个人博客

  • 显示个人博客主页
  • 显示个人博客文章详细信息:赞、踩、评论
  • 显示个人博客分类信息:标签、分类、时间
  • 进行个人博客主题自定制:后台修改

3)后台管理

  • 个人信息管理
  • 个人标签管理
  • 个人分类管理
  • 个人文章

2. 数据库设计

2.1 关系分析

1)用户表:UserInfo

用户表中存放用户的一些信息如:nid、username、password、email、avatar(头像)、nickname(昵称)以及创建时间create_time;

另外由于要构建互粉的关系,UserInfo需要和自己本身建立起多对多的关系,所以还需要构建一个ManyToMany字段和自己建立起多对多关联

这个多对多的字段fans(其实是一张多对多的表),保存用户和粉丝的对应关系。这里通过UserFans表构建自定义的ManyToMany关系。

2)互粉关系表:UserFans

互粉关系表就是保存用户和用户之间的互粉关系的,它是一种多对多的关系。

这里构建两个外键字段user和follower,都和UserInfo表的nid建立外键关联;并且user和follower这两个字段需要建立联合唯一索引,以防止条目重复。

3)博客信息表:Blog

博客信息表用来保存某用户的博客信息的,保存nid、title(个人博客标题)、site(个人博客前缀)、theme(博客主题);

此外,个人博客需要和某个用户建立关联,且是一一对应的关系,所以这里需要在Blog表中创建一个user字段和UserInfo表的nid字段建立一对一关联;

访问个人博客时,是通过url获取到博人博客前缀后,查询这里的Blog表,然后拿到对应的user字段,再通过一对一关联去查询UserInfo表获取对应用户的nid,最后再进行后续操作的

4)博主个人文章分类:Category

文章分类表中只需要定义一个title(分类标题),然后再定义一个外键字段blog和Blog表建立外键关联即可。

因为某个分类标题必须属于某个个人博客,而某个个人博客可以拥有多个分类标题,所以blog字段需要和Blog表的nid建立外键关联。

5)文章详细表:ArticleDetail

文章详细表需要定义content存储文章内容,再定义一个article字段和Article表建立一对一关联。

文章详细表需要和文章表一一对应,所以需要和文章表的nid建立一对一关联。

6)标签表:Tag

标签表和文章分类表类似,需要定义title(标签名称),还需要定义blog字段与Blog表的nid建立外键关联。

7)文章表:Article

文章表中需要包含某篇文章的所有信息,如title(文章标题)、summary(文章简介)、read_count(阅读数)、comment_count(评价数)、up_count(赞数)、down_count(踩数),以及create_time(创建时间);

某篇文章必须是属于某个个人博客的,所以需要定义一个blog字段和Blog表的nid建立外键关联(某个博客下有多篇文章);

某个博客的所有文章类型是保存在Category中的,所以某篇文章需要定义一个category字段和Category表中的nid字段建立外键关联;

此外,关于文章和标签之间的关系:某篇文章可以有多个标签,而某个标签下也可以有多篇文章,所以Article和Tag是多对多的关系;所以在Article表中要定义一个tag字段,通过这个字段来建立Article和Tag的多对多关联,这里使用自定义多对多关联表Article2Tag。

8)文章和标签的关联表:Article2Tag

文章和标签是多对多关系,所以要定义article和tag字段,分别和Article表的nid、Tag表的nid建立外键关联。

并且articel和tag字段需要建立联合唯一索引。

2.2 表结构构建

from django.db import models

class UserInfo(models.Model):
    """用户表"""
    nid = models.BigAutoField(primary_key=True)
    username = models.CharField(verbose_name='用户名', max_length=32, unique=True)
    password = models.CharField(verbose_name='密码', max_length=64)
    nickname = models.CharField(verbose_name='昵称', max_length=32)
    email = models.EmailField(verbose_name='邮箱', unique=True)
    avatar = models.ImageField(verbose_name='头像')

    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)

    fans = models.ManyToManyField(
        verbose_name='儿子们',
        to='UserInfo',
        through='UserFans',
        related_name='f',
        through_fields=('user', 'follower')
    )

class UserFans(models.Model): """互粉关系表""" user = models.ForeignKey(verbose_name='博主', to='UserInfo', to_field='nid', related_name='users', on_delete=models.CASCADE) follower = models.ForeignKey(verbose_name='粉丝', to='UserInfo', to_field='nid', related_name='followers', on_delete=models.CASCADE) class Meta: unique_together = [ ('user', 'follower'), ]
class Blog(models.Model): """博客信息""" nid = models.BigAutoField(primary_key=True) title = models.CharField(verbose_name='个人博客标题', max_length=64) site = models.CharField(verbose_name='个人博客前缀', max_length=32, unique=True) theme = models.CharField(verbose_name='博客主题', max_length=32) user = models.OneToOneField(to='UserInfo', to_field='nid', on_delete=models.CASCADE) class Category(models.Model): """博主个人文章分类""" nid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='分类标题', max_length=32) blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid', on_delete=models.CASCADE) class ArticleDetail(models.Model): """文章详细表""" content = models.TextField(verbose_name='文章内容') article = models.OneToOneField(verbose_name='所属文章', to='Article', to_field='nid', on_delete=models.CASCADE) class Tag(models.Model): """标签表""" nid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='标签名称', max_length=32) blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid', on_delete=models.CASCADE) class Article(models.Model): """文章表""" nid = models.BigAutoField(primary_key=True) title = models.CharField(verbose_name='文章标题', max_length=128) summary = models.CharField(verbose_name='文章简介', max_length=255) read_count = models.IntegerField(default=0) comment_count = models.IntegerField(default=0) up_count = models.IntegerField(default=0) down_count = models.IntegerField(default=0) create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid', on_delete=models.CASCADE) category = models.ForeignKey(verbose_name='文章类型', to='Category', to_field='nid', null=True, on_delete=models.CASCADE) type_choices = [ (1, "Python"), (2, "Linux"), (3, "OpenStack"), (4, "GoLang"), ] article_type_id = models.IntegerField(choices=type_choices, default=None) tags = models.ManyToManyField( to='Tag', through='Article2Tag', through_fields=('article', 'tag'), ) class Article2Tag(models.Model): """文章和标签的关系表""" article = models.ForeignKey(verbose_name='文章', to='Article', to_field='nid', on_delete=models.CASCADE) tag = models.ForeignKey(verbose_name='标签', to='Tag', to_field='nid', on_delete=models.CASCADE) class Meta: unique_together = [ ('article', 'tag'), ]

3. 验证码登录

3.1 程序目录结构

  • project
    • - APP(repository) - 数据仓库(操作数据Model)
    • - APP(backend) - 后台管理
    • - APP(web) - 首页,个人博客
    • - utils - 工具包(公共模块)

3.2 url路由

from django.conf.urls import url
from .views import account
from .views import home

urlpatterns = [
  # 登录
    url(r'^login.html$', account.login),
  # 验证码获取
    url(r'^check_code.html$', account.check_code),

    url(r'^home$', account.home),
]

3.3 构建Form表单

from django.core.exceptions import ValidationError
from django import forms as django_forms
from django.forms import fields as django_fields
from django.forms import widgets as django_widgets
from repository import models

# 构建一个Form表单的基类,用来存储request对象
# 用以获取session中保存的验证码信息和POST中用户填写的验证码信息
class BaseForm(object):
    def __init__(self, request, *args, **kwargs):
        self.request = request
        super(BaseForm, self).__init__(*args, **kwargs)

class LoginForm(BaseForm, django_forms.Form):
    username = django_fields.CharField()

    # password = django_fields.RegexField(
    #     '^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$\%\^\&\*\(\)])[0-9a-zA-Z!@#$\%\^\&\*\(\)]{8,32}$',
    #     min_length=12,
    #     max_length=32,
    #     error_messages={'required': '密码不能为空.',
    #                     'invalid': '密码必须包含数字,字母、特殊字符',
    #                     'min_length': "密码长度不能小于8个字符",
    #                     'max_length': "密码长度不能大于32个字符"}
    # )
    password = django_fields.CharField()

  # 一个月免登录的勾选框
    rmb = django_fields.IntegerField(required=False)

    check_code = django_fields.CharField(
        error_messages={'required': '验证码不能为空.'}
    )

  # 自定义验证check_code字段
  # 将用户填写的验证码信息和session中保存的验证码信息进行比对
    def clean_check_code(self):
        if self.request.session.get('CheckCode').upper() != self.request.POST.get('check_code').upper():
            raise ValidationError(message='验证码错误', code='invalid')

3.4 views视图函数

1)生成验证码

  • utils/check_code.py
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import random
    from PIL import Image, ImageDraw, ImageFont, ImageFilter
    
    _letter_cases = "abcdefghjkmnpqrstuvwxy"  # 小写字母,去除可能干扰的i,l,o,z
    _upper_cases = _letter_cases.upper()  # 大写字母
    _numbers = ''.join(map(str, range(3, 10)))  # 数字
    init_chars = ''.join((_letter_cases, _upper_cases, _numbers))
    
    # PIL
    def create_validate_code(size=(120, 30),
                             chars=init_chars,
                             img_type="GIF",
                             mode="RGB",
                             bg_color=(255, 255, 255),
                             fg_color=(0, 0, 255),
                             font_size=18,
                             font_type="Monaco.ttf",
                             length=4,
                             draw_lines=True,
                             n_line=(1, 2),
                             draw_points=True,
                             point_chance=2):
        """
        @todo: 生成验证码图片
        @param size: 图片的大小,格式(宽,高),默认为(120, 30)
        @param chars: 允许的字符集合,格式字符串
        @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG
        @param mode: 图片模式,默认为RGB
        @param bg_color: 背景颜色,默认为白色
        @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF
        @param font_size: 验证码字体大小
        @param font_type: 验证码字体,默认为 ae_AlArabiya.ttf
        @param length: 验证码字符个数
        @param draw_lines: 是否划干扰线
        @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效
        @param draw_points: 是否画干扰点
        @param point_chance: 干扰点出现的概率,大小范围[0, 100]
        @return: [0]: PIL Image实例
        @return: [1]: 验证码图片中的字符串
        """
    
        width, height = size  # 宽高
        # 创建图形
        img = Image.new(mode, size, bg_color)
        draw = ImageDraw.Draw(img)  # 创建画笔
    
        def get_chars():
            """生成给定长度的字符串,返回列表格式"""
            return random.sample(chars, length)
    
        def create_lines():
            """绘制干扰线"""
            line_num = random.randint(*n_line)  # 干扰线条数
    
            for i in range(line_num):
                # 起始点
                begin = (random.randint(0, size[0]), random.randint(0, size[1]))
                # 结束点
                end = (random.randint(0, size[0]), random.randint(0, size[1]))
                draw.line([begin, end], fill=(0, 0, 0))
    
        def create_points():
            """绘制干扰点"""
            chance = min(100, max(0, int(point_chance)))  # 大小限制在[0, 100]
    
            for w in range(width):
                for h in range(height):
                    tmp = random.randint(0, 100)
                    if tmp > 100 - chance:
                        draw.point((w, h), fill=(0, 0, 0))
    
        def create_strs():
            """绘制验证码字符"""
            c_chars = get_chars()
            strs = ' %s ' % ' '.join(c_chars)  # 每个字符前后以空格隔开
    
            font = ImageFont.truetype(font_type, font_size)
            font_width, font_height = font.getsize(strs)
    
            draw.text(((width - font_width) / 3, (height - font_height) / 3),
                      strs, font=font, fill=fg_color)
    
            return ''.join(c_chars)
    
        if draw_lines:
            create_lines()
        if draw_points:
            create_points()
        strs = create_strs()
    
        # 图形扭曲参数
        params = [1 - float(random.randint(1, 2)) / 100,
                  0,
                  0,
                  0,
                  1 - float(random.randint(1, 10)) / 100,
                  float(random.randint(1, 2)) / 500,
                  0.001,
                  float(random.randint(1, 2)) / 500
                  ]
        img = img.transform(size, Image.PERSPECTIVE, params)  # 创建扭曲
    
        img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)  # 滤镜,边界加强(阈值更大)
    
        return img, strs
    View Code

    需要注意的是,生成验证码前,要下载 Monaco.ttf 的字体文件放到项目根目录下。

2)验证码登录的处理逻辑

import json
from io import BytesIO
from django.shortcuts import render, redirect, HttpResponse
from repository import models
from ..forms.account import LoginForm
from utils.check_code import create_validate_code

def check_code(request):
    """验证码"""
    stream = BytesIO()                     # 开辟一片内存
    img, code = create_validate_code()     # 构建 图片 和 验证码
    img.save(stream, 'PNG')                # 将图片保存到内存中
    request.session['CheckCode'] = code    # 将验证码写入到session中
    return HttpResponse(stream.getvalue()) # 将内存中的图片返回

def login(request):
    """登录"""
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        result = {'status': False, 'message': None, 'data': None}
        form = LoginForm(request=request, data=request.POST)
        if form.is_valid():  # Form表单验证通过,Form表单验证了验证码,但是还有对验证用户名和密码验证
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            user_info = models.UserInfo.objects.\
                filter(username=username, password=password). \
                values('nid', 'nickname',
                       'username', 'email',
                       'avatar',
                       'blog__nid',
                       'blog__site').first()
            if not user_info:
                result['message'] = '用户名或密码错误'
            else:
                result['status'] = True
                request.session['user_info'] = user_info    # 将user_info信息保存到session中
                if form.cleaned_data.get('rmb'):            # 是否勾选了30天免登陆
                    request.session.set_expiry(60*60*24*30) # session保存30天
        else:
            print(form.errors)
            if 'check_code' in form.errors:
                result['message'] = '验证码错误或过期'
            else:
                result['message'] = '用户名或密码错误'
        return HttpResponse(json.dumps(result))
def home(request): return redirect('http://cnblogs.com/hgzero')

3.5 login.html模板

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="/static/plugins/bootstrap/css/bootstrap.css"/>
    <link rel="stylesheet" href="/static/plugins/font-awesome/css/font-awesome.css"/><link rel="stylesheet" href="/static/css/edmure.css"/><link rel="stylesheet" href="/static/css/commons.css"/><link rel="stylesheet" href="/static/css/account.css"/></head>
<body>
<div class="login">
    <div style="font-size: 25px; font-weight: bold;text-align: center;">
        用户登陆
    </div>
    <form id="fm" method="POST" action="/login.html">
        {% csrf_token %}
        <div class="form-group">
            <label for="username">用户名</label>
            <input type="text" class="form-control" name="username" id="username" placeholder="请输入用户名">
        </div>
        <div class="form-group">
            <label for="password">密码</label>
            <input type="password" class="form-control" name="password" id="password" placeholder="请输入密码">
        </div>
        <div class="form-group">
            <label for="password">验证码</label>

            <div class="row">
                <div class="col-xs-7">
                    <input type="text" class="form-control" name="check_code" id="check_code" placeholder="请输入验证码">
                </div>
                <div class="col-xs-5">
                    <img id="check_code_img" src="/check_code.html">
                </div>
            </div>

        </div>
        <div class="checkbox">
            <label>
                <input type="checkbox" value="1" name="rmb"> 一个月内自动登陆
            </label>

            <div class="right">
                <a href="#">忘记密码?</a>
            </div>
        </div>
        <div class="row">
            <div class="col-xs-3">
                <a id="submit" class="btn btn-default">登 陆</a>
            </div>
            <div class="col-xs-9" style="padding-left: 0;">
                <div class="alert alert-danger hide">
                    <span style="padding: 0 5px 0 5px;display: inline-block;font-size: 14px">
                        <i class="fa fa-minus-circle" aria-hidden="true"></i>
                    </span>
                    <span id="error_msg" style="font-size: 12px;"></span>
                </div>
            </div>
        </div>
    </form>
    <script src="/static/js/jquery-1.12.4.js"></script>
    <script type="text/javascript">
        $(function () {
            bindLogin();
        });
        function bindLogin() {
            $('#submit').click(function () {
                var $msg = $('#error_msg');
                $msg.parent().addClass('hide');
                $.ajax({
                    url: '/login.html',
                    type: 'POST',
                    data: $('#fm').serialize(),
                    dataType: 'JSON',
                    success: function (arg) {
                        if(arg.status){
                            location.href = '/home'
                        }else{
                            $msg.parent().removeClass('hide');
                            $msg.text(arg.message);
                            var img = $('#check_code_img')[0];
                            img.src = img.src + '?';
                            $('#password,#check_code').val('');
                        }
                    }
                })
            })
        }
    </script>
</div>
</body>
</html>

3.6 效果展示

1)登录成功

2)登录失败

  • 验证码错误
  • 用户名或密码错误

 

 2. 知识库主页

。。。^_^ 。。。

 

posted @ 2020-08-07 17:05  Praywu  阅读(782)  评论(0编辑  收藏  举报