报障系统
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
需要注意的是,生成验证码前,要下载 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. 知识库主页
。。。^_^ 。。。