本人从事和流程优化的工作,和计算机本无多大关系。后来阴差阳错渐渐自学编程,倒也找到了很多乐趣,当然也有很多坑。

这篇文章将作为记录我开发博客系统遇到的大大小小的坑中的首篇——也是卡住我最多时间思考的地方。

如何制作一个类似网易盖楼的评论系统?

问题就分解成了:

  1. 如何设计数据库的评论表
  2. 如何在视图中展示出嵌套的样式

在网上查了一些资料,

  • 评论表有的是回复和评论功用一张表,但是必须要指定每一条评论的id以及它的回复对象pid,若是第一条回复,它的回复对象则是null。
  • 也有方案是以回复和评论以两张表分开的形式存储

从降低耦合的角度出发,第二种方案更好,在第一种方案中假如需要删除评论,也许就会其他评论造成断档的问题。由于水平有限,我还是采取了第一种方案,理由是建表简单。

但是考虑到上述问题,我也设计了一个评论必须经过审核的逻辑,没有经过审核的评论是无法展示的,也就避免了展示后被删除的可能。

CREATE TABLE "Comment" (
    id INTEGER NOT NULL, 
    uid VARCHAR(50) NOT NULL, 
    rdr_name VARCHAR(20) NOT NULL, 
    rdr_mail VARCHAR(20) NOT NULL, 
    rdr_message VARCHAR(200) NOT NULL, 
    reply_id VARCHAR(50) NOT NULL, 
    reply_to_id VARCHAR(50), 
    message_date DATETIME NOT NULL, 
    approved BOOLEAN NOT NULL, 
    PRIMARY KEY (id), 
    UNIQUE (id), 
    UNIQUE (reply_id), 
    CHECK (approved IN (0, 1))
);

其中uid指向文章编号,reply_id会在每一次插入自动生成,reply_to_id就是它回复的评论的id。

有了这样的数据库,接下来就是要考虑如何提取数据的问题了。在一个文章地下,会有数十条评论,每一条评论下面还有层层嵌套的评论。查询的开销非常大,幸运的是,这样的递归查询,已经有了解决方案了——就是使用CTE(Common Table Expression),在我所用的sqlite3中版本号需要 >3.8.3。

首先遍历出所有评论,对每一条评论递归查询出它所有的回复。

with recursive
     cte(id, reply_id, reply_to_id, rdr_message, rdr_name, message_date) as (
     select id, reply_id, reply_to_id, rdr_message, rdr_name ,message_date from Comment where reply_id = 'fa102480-dd21-11e6-b1ae-f4066974556c' and approved = 1 and uid = '3928f38e-d702-11e6-94'
     union all
     select Comment.id, Comment.reply_id, Comment.reply_to_id, Comment.rdr_message, Comment.rdr_name, Comment.message_date from Comment join cte on Comment.reply_id = cte.reply_to_id
     )
select * from cte

这样就获得了一个自下而上的的评论列表。这样结构的数据很难直接看出相互的继承关系,所以需要进一步处理使其结构嵌套起来,在python后端完成。

def nest(lst):
    """
    aim to turn flatten list (which fetched from sql) to nested structure
    :param lst: list
    :return: nested list
    """
    if not lst:
        return None
    first = lst[0]
    del lst[0]
    return {"pid": first, "id": nest(lst)}

这个递归函数会形成一个嵌套的字典表示层级关系提供给jinja模板进行递归渲染。

{% macro render_comment(comment, show_btn=True) %}
    <li class="comment">
        {{ media(comment["pid"], btn=show_btn) }}
        {% if comment["id"] %}
            <ul class="comment-ul">{{ render_comment(comment["id"], False) }}</ul>
        {% endif %}
    </li>
{% endmacro %}

其中的media也是一个宏,在我的界面里面我利用了bootstrap的media列表来展示评论,具体细节就不展示了,可以替换为任意的样式。

图-1