Flask Vue.js全栈开发|第11章:私信

  • 原创
  • Madman
  • /
  • 2018-11-23 20:05
  • /
  • 0
  • 115 次阅读

flask vuejs 全栈开发-min.png

Synopsis: 上一篇文章中用户收到的新评论通知有些问题,没考虑到评论被回复的情况,本文将修复这个 BUG。除了在用户发布的文章下面评论进行互动以外,用户之间还可以发送私密的信息,别人无法看到,类似于网络聊天。目前,用户之间可以随意发送私信,如果有人骚扰你,你也只能默默接收这些垃圾私信,下一篇将实现 黑名单 功能,可以屏蔽你讨厌的用户,对方无法 Follow 你,不会收到你新文章提醒,你也不会收到对方的骚扰信息

代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.11 ,欢迎star

1. 修复 BUG

修改 back-end/app/models.py

class User(PaginatedAPIMixin, db.Model):
    ...
    def new_recived_comments(self):
        '''用户收到的新评论计数
        包括:
        1. 用户的所有文章下面新增的评论
        2. 用户发表的评论(或下面的子孙)被人回复了
        '''
        last_read_time = self.last_recived_comments_read_time or datetime(1900, 1, 1)
        # 用户发布的所有文章
        user_posts_ids = [post.id for post in self.posts.all()]
        # 用户文章下面的新评论, 即评论的 post_id 在 user_posts_ids 集合中,且评论的 author 不是自己(文章的作者)
        q1 = set(Comment.query.filter(Comment.post_id.in_(user_posts_ids), Comment.author != self).all())

        # 用户发表的评论被人回复了,找到每个用户评论的所有子孙
        q2 = set()
        for c in self.comments:
            q2 = q2 | c.get_descendants()
        q2 = q2 - set(self.comments.all())  # 除去子孙中,用户自己发的(因为是多级评论,用户可能还会在子孙中盖楼),自己回复的不用通知
        # 用户收到的总评论集合为 q1 与 q2 的并集
        recived_comments = q1 | q2
        # 最后,再过滤掉 last_read_time 之前的评论
        return len([c for c in recived_comments if c.timestamp > last_read_time])
    ...

同时,修改发表评论和删除评论的 API:

@bp.route('/comments/', methods=['POST'])
@token_auth.login_required
def create_comment():
    '''在某篇博客文章下面发表新评论'''
    data = request.get_json()
    if not data:
        return bad_request('You must post JSON data.')
    if 'body' not in data or not data.get('body').strip():
        return bad_request('Body is required.')
    if 'post_id' not in data or not data.get('post_id'):
        return bad_request('Post id is required.')

    post = Post.query.get_or_404(int(data.get('post_id')))
    comment = Comment()
    comment.from_dict(data)
    comment.author = g.current_user
    comment.post = post
    # 必须先添加该评论,后续给各用户发送通知时,User.new_recived_comments() 才能是更新后的值
    db.session.add(comment)
    db.session.commit()  # 更新数据库,添加评论记录
    # 添加评论时:
    # 1. 如果是一级评论,只需要给文章作者发送新评论通知
    # 2. 如果不是一级评论,则需要给文章作者和该评论的所有祖先的作者发送新评论通知
    users = set()
    users.add(comment.post.author)  # 将文章作者添加进集合中
    if comment.parent:
        ancestors_authors = {c.author for c in comment.get_ancestors()}
        users = users | ancestors_authors
    # 给各用户发送新评论通知
    for u in users:
        u.add_notification('unread_recived_comments_count',
                           u.new_recived_comments())
    db.session.commit()  # 更新数据库,写入新通知
    response = jsonify(comment.to_dict())
    response.status_code = 201
    # HTTP协议要求201响应包含一个值为新资源URL的Location头部
    response.headers['Location'] = url_for('api.get_comment', id=comment.id)
    return response


@bp.route('/comments/<int:id>', methods=['DELETE'])
@token_auth.login_required
def delete_comment(id):
    '''删除单个评论'''
    comment = Comment.query.get_or_404(id)
    if g.current_user != comment.author and g.current_user != comment.post.author:
        return error_response(403)
    # 删除评论时:
    # 1. 如果是一级评论,只需要给文章作者发送新评论通知
    # 2. 如果不是一级评论,则需要给文章作者和该评论的所有祖先的作者发送新评论通知
    users = set()
    users.add(comment.post.author)  # 将文章作者添加进集合中
    if comment.parent:
        ancestors_authors = {c.author for c in comment.get_ancestors()}
        users = users | ancestors_authors
    # 必须先删除该评论,后续给各用户发送通知时,User.new_recived_comments() 才能是更新后的值
    db.session.delete(comment)
    db.session.commit()  # 更新数据库,删除评论记录
    # 给各用户发送新评论通知
    for u in users:
        u.add_notification('unread_recived_comments_count',
                           u.new_recived_comments())
    db.session.commit()  # 更新数据库,写入新通知
    return '', 204

2. 用户私信

2.1 数据模型

修改 back-end/app/models.py,增加 Message 数据模型:

class User(PaginatedAPIMixin, db.Model):
    ...
    # 用户发送的私信
    messages_sent = db.relationship('Message', foreign_keys='Message.sender_id',
                                    backref='sender', lazy='dynamic',
                                    cascade='all, delete-orphan')
    # 用户接收的私信
    messages_received = db.relationship('Message',
                                        foreign_keys='Message.recipient_id',
                                        backref='recipient', lazy='dynamic',
                                        cascade='all, delete-orphan')
    # 用户最后一次查看私信的时间
    last_messages_read_time = db.Column(db.DateTime)
    ...

    def new_recived_messages(self):
        '''用户未读的私信计数'''
        last_read_time = self.last_messages_read_time or datetime(1900, 1, 1)
        return Message.query.filter_by(recipient=self).filter(
            Message.timestamp > last_read_time).count()


class Message(PaginatedAPIMixin, db.Model):
    __tablename__ = 'messages'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    sender_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    recipient_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    def __repr__(self):
        return '<Message {}>'.format(self.id)

    def to_dict(self):
        data = {
            'id': self.id,
            'body': self.body,
            'timestamp': self.timestamp,
            'sender': self.sender.to_dict(),
            'recipient': self.recipient.to_dict(),
            '_links': {
                'self': url_for('api.get_message', id=self.id),
                'sender_url': url_for('api.get_user', id=self.sender_id),
                'recipient_url': url_for('api.get_user', id=self.recipient_id)
            }
        }
        return data

    def from_dict(self, data):
        for field in ['body', 'timestamp']:
            if field in data:
                setattr(self, field, data[field])

数据库迁移:

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask db migrate -m "add private messages"
(venv) D:\python-code\flask-vuejs-madblog\back-end>flask db upgrade

2.2 RESTful API设计

创建 app/api/messages.py

from flask import request, jsonify, url_for, g, current_app
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response, bad_request
from app.extensions import db
from app.models import User, Message


@bp.route('/messages/', methods=['POST'])
@token_auth.login_required
def create_message():
    '''给其它用户发送私信'''
    data = request.get_json()
    if not data:
        return bad_request('You must post JSON data.')
    if 'body' not in data or not data.get('body'):
        return bad_request('Body is required.')
    if 'recipient_id' not in data or not data.get('recipient_id'):
        return bad_request('Recipient id is required.')

    user = User.query.get_or_404(int(data.get('recipient_id')))
    if g.current_user == user:
        return bad_request('You cannot send private message to yourself.')
    message = Message()
    message.from_dict(data)
    message.sender = g.current_user
    message.recipient = user
    db.session.add(message)
    db.session.commit()
    response = jsonify(message.to_dict())
    response.status_code = 201
    # HTTP协议要求201响应包含一个值为新资源URL的Location头部
    response.headers['Location'] = url_for('api.get_message', id=message.id)
    return response


@bp.route('/messages/', methods=['GET'])
@token_auth.login_required
def get_messages():
    '''返回私信集合,分页'''
    page = request.args.get('page', 1, type=int)
    per_page = min(
        request.args.get(
            'per_page', current_app.config['MESSAGES_PER_PAGE'], type=int), 100)
    data = Message.to_collection_dict(
        Message.query.order_by(Message.timestamp.desc()), page, per_page,
        'api.get_messages')
    return jsonify(data)


@bp.route('/messages/<int:id>', methods=['GET'])
@token_auth.login_required
def get_message(id):
    '''返回单个私信'''
    message = Message.query.get_or_404(id)
    return jsonify(message.to_dict())


@bp.route('/messages/<int:id>', methods=['PUT'])
@token_auth.login_required
def update_message(id):
    '''修改单个私信'''
    message = Message.query.get_or_404(id)
    if g.current_user != message.sender:
        return error_response(403)
    data = request.get_json()
    if not data:
        return bad_request('You must post JSON data.')
    if 'body' not in data or not data.get('body'):
        return bad_request('Body is required.')
    message.from_dict(data)
    db.session.commit()
    return jsonify(message.to_dict())


@bp.route('/messages/<int:id>', methods=['DELETE'])
@token_auth.login_required
def delete_message(id):
    '''删除单个私信'''
    message = Message.query.get_or_404(id)
    if g.current_user != message.sender:
        return error_response(403)
    db.session.delete(message)
    db.session.commit()
    return '', 204

然后,修改 app/api/__init__.py 导入 messages.py

from flask import Blueprint

bp = Blueprint('api', __name__)

# 写在最后是为了防止循环导入,ping.py文件也会导入 bp
from app.api import ping, tokens, errors, users, posts, comments, notifications, messages

3. 新私信通知

修改 back-end/app/models.py

class User(PaginatedAPIMixin, db.Model):
    ...
    def new_recived_messages(self):
        '''用户未读的私信计数'''
        last_read_time = self.last_messages_read_time or datetime(1900, 1, 1)
        return Message.query.filter_by(recipient=self).filter(
            Message.timestamp > last_read_time).count()

同时,修改添加私信和删除私信的 API:

@bp.route('/messages/', methods=['POST'])
@token_auth.login_required
def create_message():
    ...
    # 给私信接收者发送新私信通知
    user.add_notification('unread_messages_count',
                          user.new_recived_messages())
    ...


@bp.route('/messages/<int:id>', methods=['DELETE'])
@token_auth.login_required
def delete_message(id):
    ...
    # 给私信接收者发送新私信通知(需要自动减1)
    message.recipient.add_notification('unread_messages_count',
                                       message.recipient.new_recived_messages())
    ...

3.1 给别人发私信

已登录的用户在查看其它人的个人主页时,会显示 Send private message 按钮,修改 User.vue 组件:

<router-link v-if="$route.params.id != sharedState.user_id" v-bind:to="{ name: 'MessagesHistoryResource', query: { from: $route.params.id } }" class="btn btn-block u-btn-outline-purple g-rounded-50 g-py-12 g-mb-10">
  <i class="icon-bubble g-pos-rel g-top-1 g-mr-5"></i> Send private message
</router-link>

点击后,跳转到 个人资源 中的 已发送私信 路由下的 嵌套路由,比如给用户 ID 为 3 的 Susan 发送私信,将跳转到 http://localhost:8080/resource/messages/history?from=3,效果图如下:

1 给别人发私信

发送完成后,可以通过链接返回到 已发私信列表 页面,查看你给哪些用户发过私信。这里没有像博客文章资源一样依次列出每条私信,而是列出对方用户,显示你给对方发送过几条私信,她还有几条未读

要实现上面的效果,后端需要增加 API,修改 back-end/app/api/users.py

@bp.route('/users/<int:id>/messages-recipients/', methods=['GET'])
@token_auth.login_required
def get_user_messages_recipients(id):
    '''我给哪些用户发过私信,按用户分组,返回我给各用户最后一次发送的私信
    即: 我给 (谁) 最后一次 发了 (什么私信)'''
    user = User.query.get_or_404(id)
    if g.current_user != user:
        return error_response(403)
    page = request.args.get('page', 1, type=int)
    per_page = min(
        request.args.get(
            'per_page', current_app.config['MESSAGES_PER_PAGE'], type=int), 100)
    data = Message.to_collection_dict(
        user.messages_sent.group_by(Message.recipient_id).order_by(Message.timestamp.desc()), page, per_page,
        'api.get_user_messages_recipients', id=id)
    # 我给每个用户发的私信,他们有没有未读的
    for item in data['items']:
        # 发给了谁
        recipient = User.query.get(item['recipient']['id'])
        # 总共给他发过多少条
        item['total_count'] = user.messages_sent.filter_by(recipient_id=item['recipient']['id']).count()
        # 他最后一次查看收到的私信的时间
        last_read_time = recipient.last_messages_read_time or datetime(1900, 1, 1)
        # item 是发给他的最后一条,如果最后一条不是新的,肯定就没有啦
        if item['timestamp'] > last_read_time:
            item['is_new'] = True
            # 继续获取发给这个用户的私信有几条是新的
            item['new_count'] = user.messages_sent.filter_by(recipient_id=item['recipient']['id']).filter(Message.timestamp > last_read_time).count()
    return jsonify(data)

3.2 未读私信通知

别人给你发送私信了,通过上一篇文章的用户通知方式,提醒我有新的私信了。用户访问 http://localhost:8080/notifications/messages/ 将看到哪些用户给你发送了私信,如果有未读的,头像左上角显示红色小点,并分别显示多少条 新私信

要实现上面的效果,后端需要增加 API,修改 back-end/app/api/users.py

@bp.route('/users/<int:id>/messages-senders/', methods=['GET'])
@token_auth.login_required
def get_user_messages_senders(id):
    '''哪些用户给我发过私信,按用户分组,返回各用户最后一次发送的私信
    即: (谁) 最后一次 给我发了 (什么私信)'''
    user = User.query.get_or_404(id)
    if g.current_user != user:
        return error_response(403)
    page = request.args.get('page', 1, type=int)
    per_page = min(
        request.args.get(
            'per_page', current_app.config['MESSAGES_PER_PAGE'], type=int), 100)
    data = Message.to_collection_dict(
        user.messages_received.group_by(Message.sender_id).order_by(Message.timestamp.desc()), page, per_page,
        'api.get_user_messages_senders', id=id)
    # 这个用户发给我的私信有没有新的
    last_read_time = user.last_messages_read_time or datetime(1900, 1, 1)
    new_items = []  # 最后一条是新的
    not_new_items = []  # 最后一条不是新的
    for item in data['items']:
        # item 是他发的最后一条,如果最后一条不是新的,肯定就没有啦
        if item['timestamp'] > last_read_time:
            item['is_new'] = True
            # 继续获取这个用户发的私信有几条是新的
            item['new_count'] = user.messages_received.filter_by(sender_id=item['sender']['id']).filter(Message.timestamp > last_read_time).count()
            new_items.append(item)
        else:
            not_new_items.append(item)
    # 对那些最后一条是新的按 timestamp 正序排序,不然用户更新 last_messages_read_time 会导致时间靠前的全部被标记已读
    new_items = sorted(new_items, key=itemgetter('timestamp'))
    data['items'] = new_items + not_new_items
    return jsonify(data)

注意: 没有新私信的用户是按时间倒序排列,而有新私信的用户是按 时间正序 排列,是为了查看对话记录时,会更新你的 last_messages_read_time,这样可以实现你看多少条新私信,那么通知会自动减少多少条

3.3 查看对话记录

现在你知道哪些用户给你发送新私信了,要查看具体的内容,将打开两个用户之间完整的历史对话记录。对方发送的新私信,头像左上角有红色小点标记,已读的私信显示小勾

要实现上面的效果,后端需要增加 API,修改 back-end/app/api/users.py

@bp.route('/users/<int:id>/history-messages/', methods=['GET'])
@token_auth.login_required
def get_user_history_messages(id):
    '''返回我与某个用户(由查询参数 from 获取)之间的所有私信记录'''
    user = User.query.get_or_404(id)
    if g.current_user != user:
        return error_response(403)
    page = request.args.get('page', 1, type=int)
    per_page = min(
        request.args.get(
            'per_page', current_app.config['MESSAGES_PER_PAGE'], type=int), 100)
    from_id = request.args.get('from', type=int)
    if not from_id:  # 必须提供聊天的对方用户的ID
        return bad_request('You must provide the user id of opposite site.')
    # 对方发给我的
    q1 = Message.query.filter(Message.sender_id == from_id, Message.recipient_id == id)
    # 我发给对方的
    q2 = Message.query.filter(Message.sender_id == id, Message.recipient_id == from_id)
    # 按时间正序排列构成完整的对话时间线
    history_messages = q1.union(q2).order_by(Message.timestamp)
    data = Message.to_collection_dict(history_messages, page, per_page, 'api.get_user_history_messages', id=id)
    # 现在这一页的 data['items'] 包含对方发给我和我发给对方的
    # 需要创建一个新列表,只包含对方发给我的,用来查看哪些私信是新的
    recived_messages = [item for item in data['items'] if item['sender']['id'] != id]
    sent_messages = [item for item in data['items'] if item['sender']['id'] == id]
    # 然后,标记哪些私信是新的
    last_read_time = user.last_messages_read_time or datetime(1900, 1, 1)
    new_count = 0
    for item in recived_messages:
        if item['timestamp'] > last_read_time:
            item['is_new'] = True
            new_count += 1
    if new_count > 0:
        # 更新 last_messages_read_time 属性值为收到的私信列表最后一条(最近的)的时间
        user.last_messages_read_time = recived_messages[-1]['timestamp']
        db.session.commit()  # 先提交数据库,这样 user.new_recived_messages() 才会变化
        # 更新用户的新私信通知的计数
        user.add_notification('unread_messages_count', user.new_recived_messages())
        db.session.commit()
    # 最后,重新组合 data['items'],因为收到的新私信添加了 is_new 标记
    messages = recived_messages + sent_messages
    messages.sort(key=data['items'].index)  # 保持 messages 列表元素的顺序跟 data['items'] 一样
    data['items'] = messages
    return jsonify(data)

完整的效果图如下:

2 查看新私信

4. 提交代码

$ git add .
$ git commit -m "11. 私信"
$ git checkout master
$ git merge dev
$ git branch -d dev

将本地 master 分支代码上传到 Github 代码仓库中的 master 分支:

$ git push -u origin master

打上标签 tag并上传:

$ git tag v0.11
$ git push origin v0.11

代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.11 ,欢迎star

分类: Vue.js
标签: API RESTful vuejs flask 私信
未经允许不得转载: LIFE & SHARE - 王颜公子 » Flask Vue.js全栈开发|第11章:私信

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录