Welcome to my blog. You can post whatever you like!
The blog post is using markdown, and here is a simple guide. Markdown Guide
LOGIN TO POST A BLOG博客@功能实现以及消息系统更新

Author: Satori
Posted: 2017-12-10 14:53:37
Category: Django Javascript 技术
Views: 1015 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 %}"><</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 %}">></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);
}
});
}
```
## 四、总结
这样@功能以及消息系统的更新也就讲完了。不得不感叹业务逻辑的重写和新功能的添加有分不开的关系,如果之前设计的时候多想想抽象的接口和逻辑,后期添加功能也会更容易。
Read More