博客@功能实现以及消息系统更新

Author: Satori Satori

Posted: 2017-12-10 14:53:37

Category: Django Javascript 技术

Views: 657 Comments: 0

## 一、背景 在微博以及微信等现代社交媒体上,@功能已经非常常见。在任意地方只需打一个@,后面接想要@的人,就能很轻松的让对方知道这条消息。 ## 二、总体思路 虽然这个功能看起来非常简单,但是还是有不少问题需要解决。首先是原有的消息系统不能支撑这个业务。最大的问题就是之前对于消息的类型判断和`content_type`直接挂钩。如果是博客订阅,`content_type`就是博客,如果是消息回复,那`content_type`就是评论。但是,当要加入@功能的时候,这就会出问题。@人的地方有可能是博客,也有可能是评论。因此仅仅通过`content_type`是不足以支撑这个业务。 在多次考虑之后,我决定在原有Notification的基础上增加`type`字段,用于判断消息类型。如果是订阅,就是`Subscribe`,如果是评论,就是`Reply`,如果是@,就是`At`。这样根据`type`以及`content_type`就能确定消息的类型以及消息源。改进后的Notification如下。 ``` class Notification(models.Model): NOTIFICATION_TYPES = ( ('Reply', 'Reply'), ('Subscribe', 'Subscribe'), ('At', 'At'), ) unread = models.BooleanField(default=True) type = models.CharField(max_length=max([len(x[0]) for x in NOTIFICATION_TYPES]), choices=NOTIFICATION_TYPES) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') def __str__(self): return str(self.type) + " from " + str(self.content_object) ``` 有了Notification之后,就是在博客以及评论发表的时候,这则匹配@,然后提取@的用户,添加对应的Notification,这样就能实现@功能了。 ## 三、具体实现 ### 1. Notification Service的实现 就像Blog Service一样,添加一个Notification Service来处理和消息相关的操作,将之前的订阅以及评论的消息添加重写,主要是把type字段填上,同理在查询的时候,将type作为条件进行筛选,这部分代码就不展示了。 关键的部分在于@部分。这个主要是正则匹配,将发表的文字的@的用户全部抽离出来,然后添加Notification,当然没有的用户就会被过滤掉。代码如下。 ``` @classmethod def create_at_notification(cls, instance, text): name_list = re.findall('@([^\s]+)', text) res = [] for name in name_list: user = User.objects.filter(username=name) if len(user) > 0: user = user[0] notification = Notification.objects.create(type="At", content_object=instance, user=user) res.append(notification) return res ``` 接着是定义两个函数得到由博客来的@消息以及由评论来的@消息,筛选条件就是type是at,用户是请求用户,来源是博客或者是评论,代码如下。 ``` @classmethod def get_at_blog_notifications(cls, user): return user.notification_set.filter(type="At").filter(content_type__model='blog') @classmethod def get_at_comment_notifications(cls, user): return user.notification_set.filter(type="At").filter(content_type__model='comment') ``` 最后再补上未读的@消息数,也很简单了,就是筛选条件是未读,然后类型是At,代码如下。 ``` @classmethod def get_unread_at_count(cls, user): res = user.notification_set.filter(unread=True).filter(type="At") return len(res) ``` ### 2. @功能使用Notification Service 这部分主要是要在博客或者评论发表的时候,制造Notification。这个主要是通过信号连接,在发出的时候进行触发。代码如下。 ``` def post_save_comment_receiver(sender, instance, created, *args, **kwargs): if created: NotificationService.create_reply_notification(instance) NotificationService.create_at_notification(instance, instance.text) def post_save_blog_receiver(sender, instance, created, *args, **kwargs): if created: NotificationService.create_subscribe_notification(instance) NotificationService.create_at_notification(instance, instance.text) ``` 可以看到只需在原有基础上添加`create_at_notification`调用即可。 ### 3. 后端传Notification对象 这部分主要是根据用户,将由评论和博客得到的@消息进行返回,并添加属性,让前端能识别是评论还是博客得到的消息,同时标注这条消息的id,让前端知道消息的id,并将消息按照时间排列,然后分页,最后返回,代码如下。 ``` @login_required def get_at_content(request): if request.is_ajax(): user_id = request.GET.get("user_id") user = get_object_or_404(User, id=user_id) at_blog_notification_list = NotificationService.get_at_blog_notifications(user) at_comment_notification_list = NotificationService.get_at_comment_notifications(user) at_notifications = [] for ind in range(len(at_blog_notification_list)): at_blog_notification_list[ind].src = "blog" at_notifications.append(at_blog_notification_list[ind]) for ind in range(len(at_comment_notification_list)): at_comment_notification_list[ind].src = "comment" at_notifications.append(at_comment_notification_list[ind]) at_list = list() # get obj list for notification in at_notifications: obj = notification.content_object # add related notification field obj.related_notification_id = notification.id obj.unread = notification.unread obj.src = notification.src at_list.append(obj) at_list = sorted(at_list, key=lambda x: x.publish_time, reverse=True) page = request.GET.get('page') items, page_list = BlogService.get_paginated_items(at_list, page) context = {} context['items'] = items context['page_list'] = page_list context['origin_ajax_url'] = request.get_full_path() return render(request, "accounts/at_received.html", context) else: raise Http404("Page not found!") ``` ### 4. 前端返回渲染后结果 从后端得到一系列消息之后,将它进行渲染,并根据是博客还是由评论得到的At消息进行不同的渲染,添加分页等内容,并添加按键监听,使得按下详情按钮就标记已读。还需处理分页之后不同页面的ajax请求。 ``` <!--List a list of blogs--> {% load static %} {% load blog_tags %} {% for item in items %} <div class="blog-div well aos-init aos-animate" data-aos="flip-up"> {% if item.unread %} <span class="label label-danger brand-new">New</span> {% endif %} <p class="lead"><b>{{ item.author }}</b>: {{ item.text|linebreaksbr|truncatewords:30 }}</p> <p><span class="glyphicon glyphicon-time"></span> Posted on {{ item.publish_time }}</p> {% if item.src == "comment" %} <pre><a href="{% url 'blogs:archive' item.blog.id %}" target="_blank"><b><i>{{ item.blog.title }}</i></b></a> <span style="color:grey">written by {{ item.blog.author }} On {{ item.blog.publish_time }}</span></pre> <a class="btn btn-primary read-at" id="notification_{{ item.related_notification_id }}" href="{% url 'blogs:archive' item.blog.id %}#comment_{{ item.id }}" target="_blank">Get Detail <span class="glyphicon glyphicon-chevron-right"></span></a> {% elif item.src == "blog" %} <pre><a href="{% url 'blogs:archive' item.id %}" target="_blank"><b><i>{{ item.title }}</i></b></a> <span style="color:grey">written by {{ item.author }} On {{ item.publish_time }}</span></pre> <a class="btn btn-primary read-at" id="notification_{{ item.related_notification_id }}" href="{% url 'blogs:archive' item.id %}" target="_blank">Get Detail <span class="glyphicon glyphicon-chevron-right"></span></a> {% endif %} </div> {% endfor %} <!-- Pager --> <ul class="pager"> {% if items.has_previous %} <a class="btn btn-default page-link" id="prev-btn" href="{% url_replace page=items.previous_page_number %}">&lt;</a> {% endif %} {% for i in page_list %} {% if i != '...' %} {% if i != items.number %} <a class="btn btn-default page-link" href="{% url_replace page=i %}">{{ i }}</a> {% else %} <a class="btn btn-info page-link" href="{% url_replace page=i %}">{{ i }}</a> {% endif %} {% else %} <span>...</span> {% endif %} {% endfor %} {% if items.has_next %} <a class="btn btn-default page-link" id="next-btn" href="{% url_replace page=items.next_page_number %}">&gt;</a> {% endif %} </ul> <script> $(".page-link").click(function(){ event.preventDefault(); var ajax_url = $(this).attr("href"); $.ajax({ type:"GET", url: ajax_url, success: function(result) { $('#at-received').html(result); }, error: function(jqXHR, exception) { $('#at-received').html(exception); } }); }); $(".read-at").bind('click', function(event){ var notification = $(this).attr("id"); var notification_id = notification.split("_")[1]; $.ajax({ type: "GET", url: "{% url 'read_notification' %}", data: {"notification_id": notification_id}, success: function(result) { ajax_unread_at_count(); get_notification_count(); ajax_at_received(); $(this).click(); } }); }); </script> ``` ### 5. 前端用户主页面的@消息JavaScript 这部分主要是在DashBoard页面上处理点击`@me`后前端的渲染,以及得到未读@消息个数的显示,都使用ajax请求,主要函数实现如下。 ``` function ajax_unread_at_count() { $.ajax({ type: "GET", url: "{% url 'unread_at_count' %}", success: function(result) { var count = result['count']; if (count > 0) { $('#unread-ats').html(count); } else { $('#unread-ats').html(""); } } }); } function ajax_at_received() { $.ajax({ type: "GET", url: "{% url 'user_at_received' %}", data: {"user_id": {{ request.user.id }} }, success: function(result) { $('#at-received').html(result); }, error: function(jqXHR, exception) { $('#at-received').html(exception); } }); } ``` ## 四、总结 这样@功能以及消息系统的更新也就讲完了。不得不感叹业务逻辑的重写和新功能的添加有分不开的关系,如果之前设计的时候多想想抽象的接口和逻辑,后期添加功能也会更容易。

LOGIN TO LEAVE A COMMENT