网站评论系统的设计

因为多说关闭的原因,我不想再用第三方的评论插件了,想拥有自己的评论系统。之前看了django-fluent-comments,本想用这个,但是我后来一想,我何不趁这个机会,再深入学习一下相关的知识呢,自己上动手写吧。这里主要参考了一个哥们写的评论系统, rapospectre.com 。这里还要非常感谢他的帮助。

先看一下效果图。

一、设计思路:

评论整体上分为两级:

评论类型:

  1. 第一级是针对文章本身的回复;
  2. 第二级是所有针对评论的回复;

评论人类型:

  1. 登录用户。 登录用户可以直接在评论框回复留言,系统会自动识别信息;
  2. 游客。没有登录的用户也可以评论,但必须要填写用户名和邮箱。

逻辑:

新添加回复要采用异步的方式,不刷新整个页面。这次用的不是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}}&nbsp;
                    <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}}&nbsp;
                    <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,用来提交数据到数据库。

如果评论人是未登录用户,也可以发表评论,但必须要填写邮箱和用户名。在我的数据库中,我把邮箱当做唯一健,用户名你都可以不填,因为我觉得名字这东西,不重要。

邮件必须要对,我这里添加了两道验证关卡。

  1. 前端验证。
    用到了materializejs的验证方式。

      <input id="guestemail" type="email" class="validate" v-model="comment.useremail">
      <label for="guestemail" data-error="格式错误" data-success="正确">邮箱*</label>
    

    当你输入错误的邮件格式,会提示你格式错误。

  2. 后台验证
    用到了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'})

三、新添加新功能

评论系统的基本功能已经实现了,但还有许多需要优化的地方,也要添加一些新功能。

  1. 按照最新,最热两种方式排序;
  2. 设计过滤原则,黑名单;
--------EOF---------
微信分享/微信扫码阅读