简单的场景:比如是一个客户反馈系统,客户反馈的时候,我们需要针对一个话题进行回复,话题在客服和客户之间轮转回复,但并没有强制的限制要求客户和客服只能一问一答,客户和客服都可以回复多次。

复杂的场景就是如贴吧,论坛等,可以针对某个回复进行插楼回复,这种针对一个热门话题很有用,大家可以针对自己的爱好,进行插楼回复,忽略不感兴趣的话题。

先来分析第一个简单的场景,默认不支持插楼的场景,类似QQ聊天的场景,只有简单的二级关系,一级对应话题,所有的回复都是二级,在二级内都是平级关系,但是拥有先后关系,每一个回复是有先后顺序的。

简单的场景覆盖:

  • 体现回复的内容
  • 体现回复的先后顺序
  • 可辨别回复人身份
  • 支持消息状态标识

简单ER图

根据上述的描述,我们简单设计一下的存储细节:

message center design

上述结构中,我们存储了一下字段:

Column Type Description
create_time bigint 消息创建时间
status int 消息状态
user_id vchar(16) 用户ID
parent_id foreign key 回复消息ID
content text 消息内容

通过上述简单的设计,我们可以实现我们的需求,通过self - self的外键关系,我们可以通过索引是否是空来辨别那些是需求,那些是回复内容(跟帖);通过外键我们也能快速查看一个需求的所有回复,最终通过create_time来快速排序;通过user_id来获取消息创建人的角色,辨别是用户还是admin。

上面这个简单的设计中,我们主要通过表的自外键连接来快速查找到一个话题的所有的回复,但是标准的做法应该是建立一张话题表,然后再来一章评论表,评论表通过外键与话题表进行关联。

general design

这个设计上实际上和上面的并无差异,只是把话题单独抽象成一个表进行存储,适合数据量更加大的情形。

在上述设计的基础上,我来可以再来看看带有插楼功能的话题回复应该如何设计。带有插楼功能的情形是我们不仅要区分回复属于那个话题,还需要知道他的前置回复是谁,不是简单的二级关系,我们可能遭遇多级的情况,比如某一楼的回复比较有意思,那么就可能出现盖楼的情况,存在多级的情况;另外还需要考虑在同一级楼的前后顺序。

general design

上述设计中,只是增加了回复表的自外键参考结构,起的作用就是可以通过这个关系查看到当前回复的父级消息是谁,最终构造出复杂的多级关系,唯一的问题是,不太好输出最终的数据结构,解析困难。

Django 设计

针对第一种场景,我们做一下实例:


import time
from django.db import models


class Comments(models.Model):
    """user comments """

    STATUS = (
        (0, u"创建"),
        (1, u"未读"),
        (2, u"查看"),
        (3, u"关闭")
    )

    objects = CommentsManager() # 对象管理器

    create_time = models.BigIntegerField(default=time.time()) # 创建时间
    status = models.IntegerField(default=0, choices=STATUS)   # 帖子状态
    user_id = models.CharField(max_length=16, null=False, blank=False) # 创建用户
    parent_id = models.Foreign_key("self", default=None, null=True, related_name="comments") # 参考话题
    content = models.TextField(blank=False, null=False) # 回复内容


class CommentsManager(models.Manager):
    """comments manager"""

    def get_queryset(self):
        """
        Get Default Queryset
        :return: 
        """
        return super(TemplateManager, self).get_queryset()

    def get_all_topics(self):
        """
        Get all topics
        """
        return self.get_queryset().filter(parent_id=None).values(
            "status", "user_id", "status", "content")
    
    def get_all_comments_by_tid(self, tid):
        """
        Get all comments by parent id

        :param tid:
        """
        topic = self.get_queryset().get(pk=tid)
        return topic.comments().values(
            "status", "user_id", "status", "content")

上述实例只是测试了没有插楼的情形,插楼的情况下,需要我们创建两个表,如果有需要可以上述示例的基础上进行扩展。