在这篇博客中,将为大家填上上篇结尾挖的两个坑——博客的点赞功能以及使用cookie来修复博客阅读数被重复统计的bug,因此本篇可以当成上一篇的续集来看。
首先来看点赞功能的实现。与阅读数相似,点赞数也属于高频率的数据更新操作,因此我们依然使用redis来实现。点赞功能需要实现以下几点:1. 当某人给某篇博客点赞时,该博客的赞数+1,并通过上一篇实现的消息系统给博客作者发送消息;2. 当某人已经给某篇博客点过赞,则不允许其再次点赞,防止重复点赞;3. 在博客正文content页面上提供点赞链接,并显示点赞数。4. 在点赞功能中我们要限制匿名用户,即只有登录的用户才能点赞。在这里,我们需要redis的set(集合)结构来储存用户点过赞的博客ID。由于set具有排重性,所以我们用它来储存用户的点赞列表是再合适不过了。因此我们的设计思路如下:当用户是第一次给该博客点赞时,将该博客的ID加入到用户的集合中,并将博客的点赞数+1显示在正文content页面上;当用户已经点过赞,则在前端页面上会显示“已赞”且并非超链接,防止用户点击。
我们需要在blogs/views.py里新增加一个thumbup的函数作为点赞功能,代码如下:
# blogs/views.py
# ...
def thumbup(request):
try:
currentUser = Users.objects.get(username=request.session['username'])
except KeyError:
return render(request,'users/pleaselogin.html')
except Users.DoesNotExist:
return render(request, 'users/pleaselogin.html')
blogId = request.session['currblogId']
blog = Blog.objects.get(pk=blogId)
auther = blog.auther.username
userthumb_key = generateKey(currentUser.username,RedisKey['THUMBUPKEY'])
blogthumb_key = generateKey(blogId,RedisKey['THUMBCOUNTKEY'])
pool = ConnectionPool(host='localhost', port='6379', db=0)
redis = StrictRedis(connection_pool=pool,decode_responses=True)
title = ''
countOfThumb = 0
messagekey = generateKey(auther, RedisKey['UNREADMSGKEY'])
# 每个读者不能给同一篇文章多次点赞
if redis.sismember(userthumb_key,blogId):
pass
else:
redis.sadd(userthumb_key, blogId)
if redis.exists(blogthumb_key):
redis.incr(blogthumb_key)
else:
redis.set(blogthumb_key,countOfThumb)
redis.incr(blogthumb_key)
message_content = currentUser.username + u'点赞了博客' + blog.title + u'于' + str(datetime.datetime.now())
redis.lpush(messagekey, message_content)
pool.disconnect()
return HttpResponseRedirect(reverse('blogs:content', kwargs={'blogId': request.session['currblogId']}))
当当前用户不存在时,点击点赞功能会重定向到一个新的页面pleaselogin.html,该页面会引导用户前往登录页面或回到主页。随后,我们设计了两个rediskey:userthumb_key和blogthumb_key,前者用于存储当前用户的点赞列表,而后者用于记录该博客的点赞数,messagekey依然是上一篇博客中提到的消息key。我们使用sismember(key,value)来判断value是否在key所指向的set中,这里的key为userthumb_key,而value为博客的ID;当当前博客ID在当前用户的点赞列表中不存在时,我们使用sadd函数将博客ID加入到当前用户的点赞列表中,且将博客的点赞数目+1。最后,我们将点赞的消息内容拼好,通过messagekey发送给博客的作者,然后重定向到正文页面,实现点赞数的实时更新。
别忘了在blogs/urls.py中为thumbup函数加上url:
# blogs/urls.py
# ...
urlpatterns = [
# ...
url(r'^thumbup/$',views.thumbup,name='thumbup'),
# ...]
# ...
随后,我们要修改content函数,需要添加一个标志位来标志当前用户是否给当前博客点过赞,同时显示当前博客获得的点赞数量:
# blogs/views.py
def content(request,blogId):
# ...
countOfThumb = 0
blogthumb_key = generateKey(blogId, RedisKey['THUMBCOUNTKEY'])
if redis.exists(blogthumb_key):
countOfThumb = redis.get(blogthumb_key).decode()
userthumb_key = generateKey(currentusername, RedisKey['THUMBUPKEY'])
thumbflag = 'F'
if redis.exists(userthumb_key):
if redis.sismember(userthumb_key, blogId):
thumbflag = 'T'
# ...
blogContent = {
# ...
'countOfThumb':countOfThumb,
'thumbupflag':thumbflag
}
# ...
这里的思路比较简单,第一步是根据blogthumb_key拿到博客的点赞数量,第二步是到当前用户的点赞列表中查找有无当前博客的ID,若有,则将标志thumbflag置为T,反之为初始值F。最后,将点赞数countOfThumb和点赞标志位thumbflag渲染到前端:
<!--content.html-->
<!--......-->
<p>
{% if thumbupflag == "T" %}
已赞({{ countOfThumb }})
{% else %}
<a href="{% url 'blogs:thumbup' %}">赞({{ countOfThumb }})</a>
{% endif %}
<a href="{% url 'index' %}">返回首页</a></p>
<!--......-->
至于那个pleaselogin.html也很简单,放在Users App的网页目录下就好:
<!--pleaselogin.html-->
{% extends "userTemplate.html" %}
{% block content %}
<div class="content">
<p>登录后方可进行此操作</p>
</div>
<p><a href="{% url 'users:userlogin' %}">登录</a> <a href="{% url 'index' %}">返回首页</a></p>
{% endblock %}
此时,当一个用户未给博客点赞时,页面如下:
点击“赞”,则博客的赞数+1,且“赞”会变为“已赞”:
现在,我们的点赞功能已经实现,可以来填下一个坑了——使用cookies避免阅读数被重复统计。
在之前的设计中,content函数负责对阅读数进行更新,即每访问一次content页面都会对阅读数+1。这似乎没有什么问题。但是,当我测试点赞功能时,发现每点一次赞,不仅赞数增加了1,而且阅读数也增加了1!而且,每发表一个评论,该博客的阅读数也会加1。甚至,当按下F5时,博客的阅读数依然会加1!。
原因就在于,我们的发布评论和点赞功能最后都会重定向到当前的content页面,导致每发布一个评论都会执行content函数,从而对阅读数进行更新;而按F5是同样的道理,都是由于访问content函数而引起的重复更新阅读数。因此,我们需要对更新阅读数的操作做一些限制,让它有选择性地执行,而不是简单地每调用一次content函数就更新一次。
在这里,我们选用cookies的方法来限制阅读数的更新。django的cookies与session类似,也是一个字典的结构。我们选择在cookies里存放当前用户阅读过的博客ID列表,每次访问content函数时,都要先检查当前博客ID在当前用户的阅读列表中是否存在,若存在则不更新阅读数,反之再将访问数加1,这样便可避免阅读数的重复增加。
让我们继续修改blogs/views.py的content函数,在刚才的blogContent字典之后加入以下代码:
# blogs/views.py
# ...
readblog_key = generateKey(currentusername, RedisKey['READBLOGKEY'])
readblogIdlist = []
response = render(request, 'blogs/content.html', blogContent)
if readblog_key in request.COOKIES:
readblogIdlist = request.COOKIES.get(readblog_key).split(',')
if blogId not in readblogIdlist:
if redis.exists(readcount_key):
redis.incr(readcount_key)
else:
redis.set(readcount_key, blog.readcount)
redis.incr(readcount_key)
else:
if redis.exists(readcount_key):
redis.incr(readcount_key)
else:
redis.set(readcount_key, blog.readcount)
redis.incr(readcount_key)
# 添加cookie
readblogIdlist.append(blogId)
readblogIdStr = ','.join(readblogIdlist)
response.set_cookie(readblog_key, readblogIdStr, 60)
return response
我们用readblog_key来存储当前用户的阅读列表。当readblog_key存在于cookies中时,我们要将cookies中的博客ID串转换为list形式,再去判断当前的博客ID是否在这个列表中,若在,则将博客的阅读数加1;当readblog_key在cookies中不存在,则表明该用户尚未阅读过博客,因此要对博客的阅读数加1。最后,将当前博客ID加入到阅读列表中,并将列表以逗号分割后存回cookies中。在这里,我们使用response.set_cookie来设置某个页面的cookie,最后的数字为超时时间,这里为了测试设为60s,即每篇博客的阅读数在被同一个用户阅读后超过60s才会再次统计。
注意,这里我们仍然是用用户名来作为阅读列表主键的,这会导致不同的匿名用户阅读同一篇博客时阅读数只会增加一次的问题。因此,我们需要对匿名用户的机制做一些修改。由于我们的currentusername是从session中拿到的,因此我们只需对每名匿名用户生成不同的内部username存储在session中就好。
我们在myblog/views.py中的index函数中加入以下代码:
# myblog/views.py
def index(request):
try:
username = request.session['username']
user = Users.objects.get(username=username)
except KeyError:
user = Users.objects.get(username='anony')
if 'username' not in request.session:
request.session['username'] = str(uuid.uuid1())
except Users.DoesNotExist:
user = Users.objects.get(username='anony')
if request.session['username'] == '':
request.session['username'] = str(uuid.uuid1())
# ...
当前用户为匿名用户anony时,在session中存储的用户名为一个uuid,uuid是可以确保唯一性的。这样可以做到实际的用户名和内部用户名的分离处理,在content函数中生成key时也会用uuid而不是anony生成。
我们现在退出登录,然后阅读一篇博客:
第一篇博客dada的阅读数增加了1。如果我们在60s内继续点击这篇博客,阅读数不会增加。但是如果我们用手机打开的话,由于会产生新的uuid,阅读数会继续增加:
(这里为了重设60s cookie我在电脑上多点了下,所以阅读数是124)
这样,我们就通过cookie避免了阅读数的重复统计。
好了,这篇博客实现了基于redis的点赞功能以及基于cookies的阅读数统计,算是把上一篇的两个坑填上了。在后面的博客中,我会继续把实现的功能放上来,希望大家继续关注~