网站评论系统的设计
因为多说关闭的原因,我不想再用第三方的评论插件了,想拥有自己的评论系统。之前看了django-fluent-comments,本想用这个,但是我后来一想,我何不趁这个机会,再深入学习一下相关的知识呢,自己上动手写吧。这里主要参考了一个哥们写的评论系统, rapospectre.com 。这里还要非常感谢他的帮助。
先看一下效果图。
一、设计思路:
评论整体上分为两级:
评论类型:
- 第一级是针对文章本身的回复;
- 第二级是所有针对评论的回复;
评论人类型:
- 登录用户。 登录用户可以直接在评论框回复留言,系统会自动识别信息;
- 游客。没有登录的用户也可以评论,但必须要填写用户名和邮箱。
逻辑:
新添加回复要采用异步的方式,不刷新整个页面。这次用的不是ajax,而是替代品,一个前端神器——Vue.js。 关于评论系统,前端完全用Vue编写,后台只需要提供api即可。这真正实现了前后端的分离,说实话也是我在Django上的第一次尝试,感觉很好。
后台只用Django即可完成,而这里用到了很多前端的知识:Vue+scojs+materializejs等等。
二、评论回复设计
评论展示的思想: 当打开文章页面的时候,属于该文章的评论会自动以异步的方式加载。
1. 第一级:针对文章本身的回复
这个比较好理解,就是在文章下面的初次评论,该回复应该包括以下信息:
- 提交评论时间;
- IP地址;
- 评论内容;
- 评论人信息;
我们要给评论建立一张数据表,用来存储评论,其Model应该包含以下的字段:
- id 评论的id,类似于身份证,但不需要我们创建,Django自动生成;
- ip IP地址,目前只能存储IPV4地址;
- content 评论的内容;
- article 文章的外键;
- user 用户信息 ,我打算把已注册的和未注册的放到一张表里面;
-
pub_time 评论时间;
class BaseComment(models.Model): create_time = models.DateTimeField(auto_now=True) ip = models.GenericIPAddressField(protocol='IPv4') content = models.TextField()
def save(self,*args,**kwargs): """ try: ip_address_validators('ipv4',self.ip) except ValidationError: return """ super(BaseComment,self).save(*args,**kwargs) class Meta: #the class is base class,include common info,do not create table abstract = True class CommentBaseComment): article = models.ForeignKey(Article,verbose_name=u'原文名字',related_name='comment_article_name') author = models.ForeignKey(Author,related_name='pub_comment_author') def __unicode__(self): return "{0}:{1}".format(self.author, self.create_time.strftime("%Y-%m-%d %H:%M:%S")) class Meta: ordering = ['-create_time'] get_latest_by = 'create_time' verbose_name = 'comment' #unique_together = ('author','content')
写了一个基类,主要是Comment和ReplyComment都有的公共字段。
2. 第二级:所有针对评论的回复
所有针对同一个文章评论的回复都会归为二级,无论之间进行了多少次来往的回复。
二级回复紧跟着一级评论显示在文章下面,这里可以按照回复时间排序。
- id 每个回复都要有id;
- ip 当然也要有;
- content 回复的内容;
- user 用户信息;
- pub_time 回复时间;
- comment 第一级评论的外键;
- reply_to 要回复的对象;
综上所述,要设计三张表,comment,Replycomment,author。
其Model代码如下:
class ReplyComment(BaseComment):
comment = models.ForeignKey(Comment,related_name='reply_comment')
author = models.ForeignKey(Author,related_name='reply_comment_author')
reply_to = models.ForeignKey(Author,related_name='reply_to_author')
def __unicode__(self):
return "{0}@{1}".format(self.author,self.reply_to)
class Meta:
ordering = ['create_time']
verbose_name = 'replycomment'
get_latest_by = 'create_time'
#unique_together = ('author', 'content')
上面已经说了,评论会以异步方式加载,那是怎么实现的呢,从前后端两个方面说:
后台
看到这里,可能都会想到了吧,就是从数据库里获取Comment的数据,没错,但是就是要把一级评论和二级回复结合起来。代码如下:
class CommentListView(ListView):
def get(self,request,*args,**kwargs):
#the parameter args,kwargs must exists
self.article_id = kwargs.get('articleid')
article = Article.objects.get(id=self.article_id)
comment_list= Comment.objects.filter(article=article)
comments = []
if comment_list.exists():
for comment in comment_list:
replies = self.get_reply(comment)
comment = djmodel2dict(comment)
comment['replies'] = replies
comments.append(comment)
return JsonResponse({'comments':comments})
def get_reply(self,comment):
replies = ReplyComment.objects.filter(comment=comment)
if replies.exists():
replies = map(djmodel2dict,replies)
return replies
else:
return []
如果评论没有回复,replies就是空列表;如果有,就加入到replies列表中。我这里自己写了一个djmodel2dict,他将model类转为字典,以便序列化,api的接口提供的是Json数据。
from django.db import models
from django.core import serializers
import json
from datetime import datetime
def djmodel2dict(obj):
if not isinstance(obj,models.Model):
return
result = {}
for field in obj._meta.fields:
value = getattr(obj,field.name)
if isinstance(value,models.Model):
value = serializers.serialize('python',[value])[0]
if isinstance(value,datetime):
value = value.strftime("%Y-%m-%d %H:%M:%S")
try:
json.dumps(value)
except:
value = str(value)
result.setdefault(field.name,value)
return result
前端
前端的数据的展现可以采用Django标准的MTV格式,但这次我尝试了新东西,后端你需要把数据给我,前端所有一切由我前端来做。
好了,Vue出现了。Vue-resource负责http请求,vue负责渲染前端模板。关于vue的知识,我就不赘述了,就把代码贴出来吧,实践出真知。
Vuecomment,js(自定义):
Vue.config.delimiters = ['${', '}}'];
Vue.http.options.emulateJSON = true;
var vm = new Vue({
el: '#vcomments',
//delimiters: ['${', '}}'],
data: {
comment: {
content:'',
commentid:0,
toid:0,
username:'',
useremail:'',
reply:0,
csrftoken:''
},
title: '留下评论',
no_comments: true,
// comments: ''
articleid:0
'
},
methods:{
getcommentlist: function () {
this.$http.get('/dashboard/comments/article/'+this.articleid+'/list/', function (data) {
this.$set('comments', data.comments);
if(data.comments.length != 0){
this.no_comments = false;
}
});
}
},
ready: function () {
this.getcommentlist();
}
});
HTML:
<div class="row" id="vcomments">
<input type="hidden" v-model="articleid" value="{{ article.id }}">
<input type="hidden" v-model="comment.csrftoken" value="{{ request.COOKIES.csrftoken }}">
<div v-if="no_comments" style="margin-left: 5%;font-size: 20px;color: #0a95cc">
暂时无评论,快在文章下面留言吧。
</div>
<div v-else class="comment" v-for="comment in comments">
<div class="commentheader">
<img class="authorimg" src="${comment.author.fields.avator}}" >
<strong class="authorheader">${comment.author.fields.name}}</strong>
<span class="commentcreatetime">${comment.create_time}}</span>
</div>
<div class="comment-content">
${comment.content}}
<div class="actions" style="display: inline-block">
<a style="color: #0000ee" class="reply" href="#idcomment"
@click="replycomment(comment.id, comment.author.pk,comment.author.fields.name)">回复</a>
</div>
</div>
<div style="margin-left: 4%" class="commentreply" v-for="reply in comment.replies">
<div class="commentheader">
<img class="authorimg" src="${reply.author.fields.avator}}" >
<strong class="authorheader">${reply.author.fields.name}} @ ${reply.reply_to.fields.name}}</strong>
<span class="commentcreatetime">${reply.create_time}}</span>
</div>
<div class="comment-content">
${reply.content}}
<div class="actions" style="display: inline-block">
<a style="color: #0000ee" class="reply" href="#idcomment"
@click="replycomment(comment.id, reply.author.pk,reply.author.fields.name)">回复</a>
</div>
</div>
</div>
</div>
</div>
comments就是评论对象,vue从api获得数据后,就会设置comments的值为服务器api提供的数据,然后在HTML上渲染。
其实上面都是vue的知识了,可以自己看看基础知识。
3、发布评论
发布评论必须要用异步的方式,从而提高用户体验感觉。那说用 ajax去post数据,服务器端必须要提供一个api,用来提交数据到数据库。
如果评论人是未登录用户,也可以发表评论,但必须要填写邮箱和用户名。在我的数据库中,我把邮箱当做唯一健,用户名你都可以不填,因为我觉得名字这东西,不重要。
邮件必须要对,我这里添加了两道验证关卡。
-
前端验证。
用到了materializejs的验证方式。<input id="guestemail" type="email" class="validate" v-model="comment.useremail"> <label for="guestemail" data-error="格式错误" data-success="正确">邮箱*</label>
当你输入错误的邮件格式,会提示你格式错误。
-
后台验证
用到了Django自带的邮件验证,django真的很好,有很多的验证器,比如URL啊,IP地址啊,邮件啊等等。from django.core.validators import validate_email from django.core.exceptions import ValidationError def ValidateEmail(email): try: validate_email(email) except ValidationError: return False return True
游客初次填写用户名邮件,发表评论后,后台会自动创建author,并将评论保存到数据库中。与此同时,会在session中保存一个token,以便同一个游客再次评论的时候,无需重复输入用户名等信息。token的机制(我在我博客里有介绍过):
from itsdangerous import URLSafeTimedSerializer as utsr
import base64
from dailyblog.settings import NUM_PER_PAGE,DOMAIN,SECRET_KEY
class Token():
def __init__(self,security_key=SECRET_KEY):
self.security_key = security_key
self.salt = base64.encodestring(security_key)
def generate_validate_token(self,username):
serializer = utsr(self.security_key)
return serializer.dumps(username,self.salt)
def confirm_validate_token(self,token,expiration=3600*24):
serializer = utsr(self.security_key)
return serializer.loads(token,
salt=self.salt,
max_age=expiration)
如果评论人是登录用户,就很好办了,没有上面的那些考虑。
提交的评论分为两种,一是文章的评论,即第一级评论;二是针对评论的回复。程序要根据某一字段分别对这两种回复做出响应。
先说一下提交评论的逻辑: 前端点击提交,vue会发送post请求给服务器,服务器接收到请求开始处理。
Vue:
methods:{
createcomment: function () {
if(this.comment.content==''){
$.scojs_message('请输入评论内容', $.scojs_message.TYPE_ERROR);
return 1;
} else {
this.$http.post('/dashboard/comment/article/'+this.articleid+'/create/',this.comment,
{'headers':{'X-CSRFToken':this.comment.csrftoken}}
).then(function(response) {
if(response.data.msg=='success') {
$.scojs_message('评论提交成功!', $.scojs_message.TYPE_OK);
this.getcommentlist();
this.clearcomment();
} else if(response.data.msg=='emailempty') {
$.scojs_message('邮箱是必填项,方便作者与您深入交流啊!', $.scojs_message.TYPE_ERROR);
} else if(response.data.msg=='emailformat') {
$.scojs_message('邮箱格式错误,请正确填写邮箱!', $.scojs_message.TYPE_ERROR);
} else if(response.data.msg=='tokenexpire') {
$.scojs_message('token过期了,您注册一下用户吧,让我们更好交流!', $.scojs_message.TYPE_ERROR);
} else {
$.scojs_message('提交失败,请稍后再试,或者给作者发邮件留言', $.scojs_message.TYPE_ERROR);
this.clearcomment();
}
});
}
},
这里要说一点,Django带有csrftoken认证机制,请求头中要带有X-CSRFToken字段。csrftoken中通过html中获得。
一级评论就是按照上面的方法处理,二级评论就需要再添加一下需要发送给服务器的字段内容,比如你是评论哪个comment啊,你要回复谁啊等等。
vue:
replycomment: function (commentid,toid,name) {
this.comment.commentid = commentid;
this.comment.toid = toid;
this.comment.reply = 1;
this.title = '@'+name+':';
},
后台:
class CreateCommentView(CreateView):
"""
the comment should divide into two categories:
1. the comment for article.
Comment
2.the reply for the comment.
ReplyComment
In additions,the author may be guest or logined user
As for logined user,it's very easy,however the guest may be
a little complex.
the request should post several parameters:
1.content
2.reply
3.commentid
4.toid
5.username
6.useremail
and the kwargs should contains articleid.
Test:
curl -X POST -H 'X-CSRFToken:7FzJqhh3EIjYyvRdD0EuJOSrPjcyAn54'
-d "content=testajaxcsrfpost&username=testpost&useremail=hbnnlong@163.com"
-b 'csrftoken=7FzJqhh3EIjYyvRdD0EuJOSrPjcyAn54'
http://localhost/dashboard/comment/article/1/create/
"""
model = Comment
#@method_decorator(csrf_exempt)
def post(self, request, *args, **kwargs):
content = request.POST.get('content')
reply = request.POST.get('reply','-1')
articleid = kwargs.get('articleid')
self.message = ''
if reply == '1':
#if reply is 1,the comment is a reply comment
comment = ReplyComment()
commentid = request.POST.get('commentid')
original_comment = Comment.objects.filter(pk=commentid)
if not original_comment.exists():
self.message = 'comment dose not exists'
return JsonResponse({'msg':self.message})
comment.comment = original_comment[0]
toid = request.POST.get('toid')
comment.reply_to = Author.objects.get(pk=toid)
else:
#or else,the comment is first comment
comment = Comment()
article = Article.objects.filter(pk=articleid)
if not article.exists():
self.message = 'article does not exists'
comment.article = article[0]
if not content:
self.message = 'please input message '
else:
comment.content = content
#the author field,it is a little complex
user = request.user
if not user.is_authenticated():
#return JsonResponse({'user':user})
confir_token = Token()
commenttoken = request.session.get('commenttoken',False)
if commenttoken:
#the token is in session.
try:
email = confir_token.confirm_validate_token(commenttoken)
author = Author.objects.get(email=email)
except:
return JsonResponse({'msg':'tokenexpire'})
else:
email = request.POST.get('useremail','')
name = request.POST.get('username',u'游客')
if not email:
return JsonResponse({'msg':'emailempty'})
if not ValidateEmail(email):
return JsonResponse({'msg':'emailformat'})
token = confir_token.generate_validate_token(email)
request.session['commenttoken'] = token
author = Author.objects.filter(email=email)
if not author.exists():
author = Author()
author.name, author.email = name, email
author.save()
author = Author.objects.get(email=email)
else:
author = author[0]
else:
author = Author.objects.filter(name=user.username)
if author.exists():
author = author[0]
else:
author = Author()
author.name = user.username
author.email = user.email
author.save()
comment.author = author
comment.ip = get_real_ip(request)
comment.save()
self.message = 'success'
return JsonResponse({'msg':'success'})
三、新添加新功能
评论系统的基本功能已经实现了,但还有许多需要优化的地方,也要添加一些新功能。
- 按照最新,最热两种方式排序;
- 设计过滤原则,黑名单;
微信分享/微信扫码阅读