设计并实现站内消息接收功能——用户行为的消息提醒

934 阅读15分钟

当平台有其他用户的行为影响到我们时,我们就会接收到平台的提醒消息,这就是站内消息系统的核心业务。

消息大体可以分为两类,一是用户产生的消息,二是系统产生的消息。

先说用户产生的消息,比如用户点赞、收藏、评论某个主体时,就会触发当前主体的发起者。

这个我们暂时借鉴下掘金的,以下是掘金中的消息回馈数据:

image.png dst_info是目标源,也就是当前的操作体,比如点赞、评论或回复的内容体,是文章、评论还是回复等等。

image.png 需要返回的基本字段有detail——内容;id和id的type。是否点赞。

message:message其实就是消息表的数据,主要包括动作actiontype,目标源的类型和id(也就是这个动作直接操作的对象),

当平台有其他用户的行为影响到我们时,我们会接收到平台的消息,这个消息就是站内消息。

还有个主要的是parent_info,即这个动作触发的基本主体信息。

那么就可以开始构思表该如何设计了。

从最简单的点赞开始做起,比如对文章点赞和对评论点赞,其实用一张表就好,通过字段进行区分。

点赞表的设计应该就这样:


module.exports = function(sequelize, DataTypes) {

  return sequelize.define('support', {

    id: {

      autoIncrement: true,

      type: DataTypes.INTEGER,

      allowNull: false,

      primaryKey: true

    },

    userid: {

      type: DataTypes.INTEGER,

      allowNull: true

    },

    targetid: {

      type: DataTypes.INTEGER,

      allowNull: true

    },

    targettype: {

      type: DataTypes.STRING(255),

      allowNull: true,

      comment: "0句子,1文章,2图书"

    },

    createtime: {

      type: DataTypes.DATE,

      allowNull: true

    }

  }, {

    sequelize,

    tableName: 'support',

    timestamps: false,

    indexes: [

      {

        name: "PRIMARY",

        unique: true,

        using: "BTREE",

        fields: [

          { name: "id" },

        ]

      },

    ]

  });

};

通过targettype和taregetid去区分,当前点赞的是主体是谁,通过主体类型和主体id去查询点赞的主体。

再来好好想想什么样的主体是最基本的主体?

最基本的主体,就是说它和其他主题是完全分开的,彼此互相不关联的,这就是基本主体,比如某个系统中,图书是一类,文章是一类,那么这些主体就是基本主体,他们即便可能存在关系,但这种关系不会是强相关的,也就是说,文章可以依赖图书,也可以独立于图书,就比如在掘金中,专栏之于图书,也是一样的道理,所以专栏和文章,都算是基本主体。

那什么样的数据不是基本主体呢?

就比如评论,用户可以对评论进行点赞,进行回复等其他各种操作,所以其实评论也是一种主体,但这类主体不能脱离关联主体的存在,也就是说,评论的存在必须基于某个主体,空洞的评论是不存在的,也就是说,评论可以针对文章评论,可以针对专栏评论,也可以针对文章中某个划线的句子评论,甚至针对评论本身或者评论的评论进行评论(回复),无论如何,评论必须有个主体前提。

我们这就考虑好了主体类型的关联和区别,那么我们就这样规定:

系统既定的targettype,从1到9,比如我们一开始就考虑好了的的主体内容,用户啊,文章啊之类的。

后期自定义的targettype,从10到100,假如后续开发突然更改需求,文章是不是该做个专栏集合,那么我们就可以指定一个10到100的类型标识专栏。

而指定关系的targettype,则从101开始,比如在点赞表中的一条数据,点赞的主体是一个评论或一条回复,那么点赞表中记录的targettype就是101.

接下来,就是消息表,消息表该如何设计呢?

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('message', {
    id: {
      autoIncrement: true,
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true
    },
    targettype: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    targetid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    messagetype: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    messageid: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    seuserid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    reuserid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    createtime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    readtime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    isread: {
      type: DataTypes.STRING(255),
      allowNull: true
    }
  }, {
    sequelize,
    tableName: 'message',
    timestamps: false,
    indexes: [
      {
        name: "PRIMARY",
        unique: true,
        using: "BTREE",
        fields: [
          { name: "id" },
        ]
      },
    ]
  });
};

消息表跟点赞表其实没有多大关系了,首先点赞点赞数据不是主体,也不是关系主体,它只是记录数据和状态,所以消息表最好也是加上额外的targettype和targetid,消息表上targettype和targetid,也就是消息触发的主体是哪个。

比如用户B点赞了一篇文章,触发了文章的作者A,那么添加的消息中targettype就是文章类型,targetid就是文章id。

同样的,点赞了文章下的一篇评论,targettype就是评论类型,taregetid就是评论id,这个应该很好理解。

掘金应该额外还设计了字段,即记录当前主题的基本主体,比如一条评论回复,它还额外记录了该回复的最基本主体,也就是文章或专栏(但实际上这个功能没用起来)

关键是messagetype和messageid,messagetype标识触发的动作,而messageid则标识当前动作的目标id。

这个messagetype和targettype不同,这个不是用来区分主体的,而是区分动作的,而messageid可以根据这个动作类型的不同,去自动匹配id(当然,是主体id才会去匹配,如果是纯关系的内容,比如点赞或收藏,就没必要去匹配这个id了)

比如说,messagetype规定1点赞,2收藏,3评论,那么messagetype为1或2时,messagetype就时null状态,因为没有消息内容,对,这个message就理解成消息内容就好。

当messagetype为3时,即意味着评论,那么评论是有消息内容的,因此messageid就指向评论id,我们就需要去拿这个message了。

基于此,我们先完成简单的通知,即点赞收藏的通知。

首先我们获取消息,首先有个统计消息数量的功能,也就是去拿未读的数据的统计:

    console.log(params);
    return await Model.message.findAll({
      where: {
        isread: 0,
        reuserid: params.userid,
      },
      attributes: [
        "messagetype",
        [
          Model.sequelize.fn("COUNT", Model.sequelize.col("messagetype")),
          "count",
        ],
      ],
      group: ["messagetype"],
    });
  },

返回的结构如下:

image.png 前端针对该数据做一次封装,总量数据有多少,评论回复数据有多少,点赞或收藏的消息有多少:

      getUserMessageStatic().then(res => {
        let { code, data } = res;
        if (code == 10000) {
          data && data.forEach(item => {
            this.readStatic.total += item.count
            if (item.messagetype == 3) {
              this.readStatic.replay = item.count
            }
            if (item.messagetype == 1 || item.messagetype == 2) {
              this.readStatic.remind += item.count
            }
          })
        }
      })
    },

那么展示效果就是这样了:

image.png

这个数据目前只是后台增加的静态数据,需要动态构造,那么就在用户点赞或收藏时,添加动态消息的功能。

后台的逻辑代码,controller层:

    let params = req.body
    if(params.userid==req.body.reuserid) return res.send({
      msg:'不能给自己发送消息'
    })
    let result = await userActionDao.sendMessage({
      seuserid:params.userid,
      reuserid:req.body.reuserid,
      targettype:params.targettype,
      targetid:params.targetid,
      messagetype:params.messagetype,//1点赞,2收藏,3评论
      messageid:params.messageid,
      createtime:new Date,
      isread:0
    })
    res.send({
      msg:'成功',
      data:result
    })
  },

这个基本逻辑其实很简单,首先拿到发送者seuserid以及接收者reuserid,seuserid是通过token解析得来的,并对这两个id进行比对,如果是相同userid则跳过这个逻辑(其实原则上来说,自己是不能给自己的文章点赞或收藏的,知乎就是这么做的,要加个这样的判断其实也挺简单的,不要前端去判断,而是交给后台,因为前端往往存储的还是token,所以需要后台在userAction时做一次统一判断。)

我们必须要记录的值有reuserid接收者,seuserid发送者,targettype目标类型,targetid目标id,messagetype动作类型,messageid动作相关的id,isreade是否阅读。

实际上这里类型判断过于简单了,可以用插件joi对这里的类型重新做个判断:

    let params = {
      seuserid:req.body.userid,
      reuserid:req.body.reuserid,
      targettype:req.body.targettype,
      targetid:req.body.targetid,
      messagetype:req.body.messagetype,//1点赞,2收藏,3评论
      messageid:req.body.messageid,
      createtime:new Date,
      isread:0
    }
    if(params.userid==params.reuserid) return res.send({
      msg:'不能给自己发送消息'
    })

    let {error,value} = Joi.object({
      seuserid:Joi.number().required(),
      reuserid:Joi.number().reuserid(),
      targettype:Joi.number().required(),
      messagetype:Joi.number().required(),
      isread:Joi.number().required(),
    }).unknown().validate(params)
    if(error) return res.send(error)
    let result = await userActionDao.sendMessage(value)
    res.send({
      msg:'成功',
      data:result
    })
  },

我们规定了哪些数据所需要的是什么类型并且是必传的,否则抛出错误。

(其实刚才遇到个错误:Joi.number(...).reuserid is not a function,我到处看为什么required会报错,前面也用到了required,没有报错,就这里报错,啊,脑壳想青痛,网上也找不到答案,啊啊,然后又去翻包里的源码,找不出问题,啊啊啊啊啊!然后放弃,然后准备删掉这个验证方法,然后并刷了会儿手机,然后再来看看我写的……才发现自己写的是reuserid!啊啊啊啊啊啊啊!!)

那么在dao层,直接插入数据就可以了,这里我做了一层判断,就是点赞和收藏这类没有消息内容的数据,我会通过targettype和targeti确定当前触发的主体,以及发出者、动作和接收者,对数据进行删除,然后在新增数据:

    // 只能删除点赞和收藏的数据,如果是评论,则不需要删除
    if(params.messagetype!=3){
      await Model.message.destroy({
        where: {
          targettype: params.targettype,
          targetid: params.targetid,
          messagetype: params.messagetype,
          reuserid: params.reuserid,
          seuserid: params.seuserid,
        },
      });
    }
    
    return await Model.message.create(params);
  },

那么前端的触发消息通知,这里的sendMessage我是写在vue实例下面的,每次触发消息时直接调用即可,即:

this.$sendMessage({
    targettype:1,//点赞文章
    targetid:this.detail.id,//点赞的文章id
    reuserid:this.detail.userid,//需要通知的用户
    messagetype:1,//1点赞,2收藏,3评论
    // messageid:res.data.id,//因为是点赞动作,所以不用记录messageid
})

收藏也是一样的,那么被通知的用户在站内就会有红点消息提示,这个数据提示就是真实的消息提示了:

image.png

如何去处理列表呢?

其实按照掘金的那种思路,我们其实就去拿message表下的消息数据,并且根据主体type和id,去关联主体内容就好了,通过联合查询,把数据结构统一做下跳转(不过这个拼接sql挺麻烦的,但在我这里性能不是关键问题,主要是理解业务逻辑,所以采用最简单的办法。)


SELECT 
        id,
        seuserid as seuserids,
        reuserid,
        createtime, targettype,targetid,messagetype,isread,messageid,
        1 as total,
              (SELECT username from user WHERE message.reuserid = user.id) reusername,
              (SELECT headimg from user WHERE message.reuserid = user.id) reheadimg,
              (SELECT username from user WHERE message.seuserid = user.id) seusernames,
              (SELECT headimg from user WHERE message.seuserid = user.id) seheadimgs
        FROM message
        where reuserid = :userid
        AND (isread = 0 OR messagetype NOT IN (1, 2))
        order by createtime desc
        )messagelist
        WHERE messagelist.messagetype in (${params.messagetype.join(",")})

前端把这个列表拿到并展示列表即可:

image.png

那什么时候修改为已读呢?

实际上,应该在拉取数据时,就自动将这些数据列表做已读处理,即在dao层中添加这段逻辑代码:

      if (ids.length > 0) {
        Model.message.update(
          {
            isread: 1,
          },
          {
            where: {
              id: {
                [Model.Sequelize.Op.in]: ids.map(id => parseInt(id)),
              },
            },
          }
        );
      }

还有个问题,就是比如点赞收藏这类的数据,本身没有内容消息,大量的点赞和收藏出现在列表页面是非常令人难以忍受的,所以聚合数据是极为必要的。 那么这就不得不说一句,为什么我会把消息通知拆分为回复,点赞收藏,新增粉丝用户以及系统消息了。

系统消息这个另说,其实回复、点赞、收藏和新增用户粉丝,其实都是message表中的内容,只是targettype不同以及messagetype不同而已,为什么要如此区分呢?

主要原因就在于数据是否需要聚合,评论回复因为数据是携带消息内容的,所以不能聚合,但新增粉丝也是需要聚合的,为啥又不跟点赞收藏汇总到一起呢,因为新增粉丝的聚合条件和点赞收藏不一样,它是以时间为单位进行聚合的,比如某个时间节点内新增了多少粉丝,而点赞收藏则不以时间单位聚合,它只跟主体和动作有关。

并且还有一点,就是未读消息不要聚合,已读消息才聚合,所以,后台增加聚合相关的逻辑代码如下:

try {
      // 我这里就贪快,尚未阅读过的数据,不聚合,阅读过的数据,聚合
      let sql = `
      select * from (

        SELECT 'juhe' as juhe,
          GROUP_CONCAT(message.id) as ids,
          GROUP_CONCAT(seuserid) as seuserids,
          reuserid,
          MAX(message.createtime) as createtime, targettype,  targetid, messagetype, isread,null as messageid,
          count(*) as total,
          (SELECT username from user WHERE message.reuserid = user.id) reusername,
          (SELECT headimg from user WHERE message.reuserid = user.id) reheadimg,
          GROUP_CONCAT(username) seusernames,
          GROUP_CONCAT(headimg) seheadimgs
        from message left JOIN user on user.id = message.seuserid
        WHERE reuserid = :userid
        and isread = 1
        and messagetype in (1,2)
        GROUP BY targetid, targettype, messagetype
        
        -- 只有点赞收藏且已经阅读过的数据,才可聚合
        UNION ALL
        SELECT 'not_juhe' as juhe,
        id as ids,
        seuserid as seuserids,
        reuserid,
        createtime, targettype,targetid,messagetype,isread,messageid,
        1 as total,
              (SELECT username from user WHERE message.reuserid = user.id) reusername,
              (SELECT headimg from user WHERE message.reuserid = user.id) reheadimg,
              (SELECT username from user WHERE message.seuserid = user.id) seusernames,
              (SELECT headimg from user WHERE message.seuserid = user.id) seheadimgs
        FROM message
        where reuserid = :userid
        AND (isread = 0 OR messagetype NOT IN (1, 2))
        order by createtime desc
        )messagelist
        WHERE messagelist.messagetype in (${params.messagetype.join(",")})
    `;

    console.log('__________',params.messagetype.includes('3'))


      let [results] = await Model.sequelize.query(sql, {
        replacements: {
          // isread:0,
          userid: params.userid,
          // messagetype:`(${params.messagetype.join(',')})`
        },
      });
      let ids = [];
      // 这里性能不是关键问题,暂时采用这种简单的写法。
      for (let item of results) {
        // 文章
        if (item.targettype == "1") {
          item.target = await Model.circle.findOne({
            where: {
              id: item.targetid,
            },
          });
          // 评论
        }else if(item.targettype == '101'){
          item.target = await Model.comment.findOne({
            where:{
              id:item.targetid
            }
          })
        }

        // 对于回复
        if(item.messagetype == '3'&&item.messageid){
          // 对文章的评论
          item.messagetarget = await Model.comment.findOne({where:{id:item.messageid}})

        }
        console.log('_______________________---',item.isread)
        if (item.isread == 0) {
          ids.push(item.ids);
        }
      }
      ids = ids.join(',').split(',')
      console.log(ids)
      // 获取到这些数据后,需要手动更新,但这里不再需要等待了
      if (ids.length > 0) {
        Model.message.update(
          {
            isread: 1,
          },
          {
            where: {
              id: {
                [Model.Sequelize.Op.in]: ids.map(id => parseInt(id)),
              },
            },
          }
        );
      }

      return results;
    } catch (err) {
      console.log(err);
    }

所以就点赞收藏的数据而言,当我拉取这段数据的时候,拉取过来的代码是未读状态,但由于返回数据前做了异步操作,此时数据库拉取的数据已经变为已读了,所以用户需要手动更新导航栏的未读数据,重新请求未读messageStatic,我这里再用另外一个账号添加三条点赞和收藏数据:

image.png

那么用户下次再进入消息通知也,看到的就是聚合后的数据了,并且时间更新为用户最新点赞和收藏的时间,即:

image.png 前端的逻辑其实挺简单的:

    <div v-for="(item,index) in messageLst" :key="index">
      
      <div class="user">
        <div v-if="item.juhe =='juhe'">
          <div class="circle" v-if="item.isread == '0'"></div>
          <img v-for="(item_s,index_s) in item.seheadimgs" v-if="index_s<=3" :src="$setImg(item_s)">
          <span class="username">等{{ item.total }}人</span>
          <span style="margin: 0 5px;">{{ item.messagetype ==1?'点赞':'收藏' }}</span>
          了你的
          <span v-if="item.targettype==1">
            文章            
          </span>
          <span v-if="item.targettype==101">
            评论            
          </span>
          <span v-if="item.targettype == 2">
            句子
          </span>
        </div>
        <div v-else>
          <div class="circle" v-if="item.isread == '0'"></div>
          <img :src="$setImg(item.seheadimgs)">
          <span class="username">{{ item.seusernames }}</span>
          <span style="margin: 0 5px;">{{ item.messagetype ==1?'点赞':'收藏' }}</span>
          了你的
          <span v-if="item.targettype==1">
            文章            
          </span>
          <span v-if="item.targettype==101">
            评论            
          </span>
          <span v-if="item.targettype == 2">
            句子
          </span>
        </div>
        <div> {{$formatTime(new Date(item.createtime))}}</div>
      </div>
      <div class="content_box">
        <div class="target_content">
          {{ item.targettype==1?item.target.title:item.target.content }}
          <!-- {{item.target.title}} -->
        </div>
        <div style="height: 1px;width: 100%;background-color: #f0f0f0;margin: 10px 0;">
        </div>
      </div>
    </div>

前端就主要通过字段juhe,判断当前数据是聚合还是单个数据,然后确定不同的展示状态,taregettype这里写的不好,这里最好提成配置,或者写在data里,但最规范的写法,还是后台做成字典表的格式。 比如这样:

let targetList = [

  {
    targettype:1,
    label:'文章'
  },
  {
    targettype:101,
    label:'评论'
  }
]

再看看数据库添加的数据是否正确:

87	1	125	2	1	2	2023-11-24 07:05:12		1	
88	1	123	1	1	2	2023-11-24 07:49:35		1	
89	1	122	1	1	2	2023-11-24 07:49:36		1	
90	1	124	1	1	2	2023-11-24 07:49:38		1	
91	1	125	1	1	2	2023-11-24 07:49:39		1	
92	1	124	2	1	2	2023-11-24 07:49:41		1	
93	1	121	1	4	2	2023-11-24 08:10:26		1	
94	1	121	2	4	2	2023-11-24 08:10:27		1	
95	1	118	2	4	2	2023-11-24 08:10:29		1	
96	101	194	1	4	2	2023-11-24 08:20:49		1	

没有问题,第二列表示targettype,1是文章,101是评论或回复,那么到这里,点赞和收藏也就算做完了,接下来就是评论功能。

评论表是个特殊的主体表,其特殊之处就在于,评论本身是主体,但它又必须依赖其他主体,所以在评论表中的字段里,不可或缺出现taregettype和targetid这样的字段。 那么这里就会出现一个问题,回复里的targettype到底是父级评论还是最外层的targettype。

这个逻辑要考虑清楚,否则后面的业务代码会相当糟糕!

由于评论表做的是多级结构:

const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
  return sequelize.define('comment', {
    id: {
      autoIncrement: true,
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true
    },
    pid: {
      type: DataTypes.INTEGER,
      allowNull: true,
      defaultValue: 0
    },
    targettype: {
      type: DataTypes.INTEGER,
      allowNull: true,
      defaultValue: 1,
      comment: "0:对句子的评论,1:对文字,2:对图书"
    },
    targetid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    content: {
      type: DataTypes.TEXT,
      allowNull: true
    },
    userid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    createtime: {
      type: DataTypes.DATE,
      allowNull: true
    }
  }, {
    sequelize,
    tableName: 'comment',
    timestamps: false,
    indexes: [
      {
        name: "PRIMARY",
        unique: true,
        using: "BTREE",
        fields: [
          { name: "id" },
        ]
      },
    ]
  });
};

所以理论上,回复表的默认targettype和targetid都确认了,就是其父级,那么其表targettype和targetid还需要记录父级的targettype和targetid吗,其实并不需要,因为这里pid就已经做到了。

因此,targettype和taregetid就是最外层的基本主体,而这样设计主要是为了这样一个好处:比如我们要删除文章时,有需要的可能会去删除文章下所有的关联数据,比如点赞、评论等等,那么如果删除评论,那么,当没有targetid和targettype做列表关联时,我们要删除评论表及其子数据,就不得不使用递归查询进行删除,而如果又targettype和targetid,那么删除文章的评论数据就相当简单了。

再回过来说targettype在message身上的表现,这个时候就需要判断了,当前targettype是基于谁的,比如基于文章发布了评论,那么targettype就是1,targetid就是文章id,那如果是针对文章下的某个评论或回复进行回复,那么targettype就是101,targetid就是对用的回复或评论id。

这个能理解清楚,就可以进行下一步了——添加评论。

比如下面代码中,我对某篇文章添加了一条评论,那么很明显,这里的targettype和targetid都是文章的,在下面的sendMessage中,targettype和targetid也是文章的,而由于评论是具有内容的消息,所以messageid需要指定新增的评论id:

      addComment({
        pid: 0,
        targetid: this.targetid,
        content: value,
        targettype: this.type
      }).then(res => {
        this.$emit('callback', ++this.commentCount)
        this.$sendMessage({
            targettype:1,
            targetid:this.targetid,
            reuserid:this.target.userid,
            messagetype:3,//评论
            messageid:res.data.id,
          })
        this.getComment()
      })

这段代码,是对评论或回复添加回复,pid就是当前评论的id,targetid和targettype都是主体的type和id,跟这个评论无关,然而添加通知消息时,这时的评论对象已经由文章主体变为评论或回复主体了,所以targettype变为评论101,对应的targetid也就是当前评论或回复的id,并且同样的,messageid为新添加的回复id:

      addComment({
        pid: item.id,
        targetid: this.targetid,//其实回复的targetid是没用的。还是指向主体
        content: content,
        targettype: this.type,//当然,包括回复的targettype也是没用的,统一做处理,回复的targettype和targetid和pid为0的节点一致。
      }).then(res => {
        this.showAllReply(piditem)
        // return console.log(res)
        this.$sendMessage({
            targettype:101,
            targetid:item.id,
            reuserid:item.userid,
            messagetype:3,//评论
            messageid:res.data.id,
          })
        // this.getComment()
      })

发布评论后,用户将在通知页面得到提示:

image.png

根据前面拉取消息列表的逻辑规则,拉取过的数据将会修改为已读状态,那么回复数据被拉取后,数据库数据就变更为已读了:

image.png

所以这里messagetype为3没有数据,1和2为点赞或收藏的数据。

这里调用getUserMessageList的接口和上面写的获取点赞和收藏的接口一样。

点击回复,同样地,需要添加回复列表以及发送通知消息,前端代码逻辑如下:

      addComment({
        pid: item.messagetarget.id,
        targetid: item.messagetarget.targetid,
        content: content,
        targettype: item.messagetarget.targettype
      }).then(res => {
        // this.showAllReply(piditem)
        // return console.log(res)
        this.$sendMessage({
            targettype:101,
            targetid:item.messagetarget.id,
            reuserid:item.userid??item.seuserids,
            messagetype:3,//评论
            messageid:res.data.id,
          })
        // this.getComment()
      })

需要注意,这段代码中,item所代表的列表项,其实是消息项,前面提到过,评论是属于携带消息内容的消息体,也就是messagetarget表示当前发出者发出的评论,而target则是这个评论所针对的主体,这个target即可以是评论,也可以是基本主体比如文章。

那么这段对当前发送者发送的消息进行回复时,前端代码逻辑就该这样写:

      addComment({
        pid: item.messagetarget.id,
        targetid: item.messagetarget.targetid,
        content: content,
        targettype: item.messagetarget.targettype
      }).then(res => {
        // this.showAllReply(piditem)
        // return console.log(res)
        this.$sendMessage({
            targettype:101,
            targetid:item.messagetarget.id,
            reuserid:item.seuserids,
            messagetype:3,//评论
            messageid:res.data.id,
          })
        // this.getComment()
      })

这里messagetarget就是当前的评论,所以pid就是item.messagettarget.id,同样的,由于评论的targettype和targetid都是共同的,所以这里直接使用messagetarget的targettype和targetid即可。

addCommment添加评论之后,就需要对该用户发送消息,由于是回复,所以targettype指定为101(主体为评论或回复类型),targetid则是当前评论的id即messagetagett.id,要通知的人,则是当前消息的发出者,即seuserids,messagetype为3,messageid即当前插入的评论id。

基本功能完成了,现在验证该功能,在用户【月亮】发布的文章下发布一条评论:

image.png

发布评论时并提交通知,编写文章者将收到一条消息:

image.png

这里就关注两个点,一个是target,这是所有消息都必有的东西,这代表动作触发的目标,即评论这篇文章的,所以target就指向这篇文章。

第二个是messagetype,只有当messagetype为3时,才会携带的参数,这代表当前消息的回复内容,也就是发送消息的这条评论本身。

回复这条评论:

image.png

再回到之前那个页面,可以查看到当前评论的回复:

image.png

并且用户也接收到了该消息:

image.png

进入消息页面之后,也能查询到该消息:

image.png

那么业务逻辑这块儿基本就没什么大问题了,所以接下来就是处理消息列表,消息列表可以查看到某条评论的列表数据,这里不能通过targettype和targetid去查数据,因为那个只绑定了基本主体数据,无法确定评论的嵌套关系,所以只能通过pid和id进行sql递归查询。

下面写个查询回复列表的接口,获取回复里列表的controller:

  async getReplay(req,res){
    if(!req.query.id) return res.send('没有id')
    let token = req.headers['authorization']
    let params = req.query
    if(token){
      let user = await jwtService.varifyToken(token)
      params.userid = user.id
    }
    let result = await userActionDao.getReplay(params)
    result.forEach((s, i, l) => {
      let target = l.filter(item => item.id == s.pid)
      if (target.length > 0) {
        s.target = {
          username:target[0].username,
          id:target[0].id,
          content:target[0].content,
          headimg:target[0].headimg
        }
      }
    })
    res.send({
      msg:'成功',
      code:10000,
      data:result
    })
  },

获取回复里列表的dao层:

    let sql = `
        WITH RECURSIVE folder_recursion AS (
          SELECT id, pid, content, createtime,userid,
          IF(COMMENT.userid=:userid,1,0) isyour,
          (SELECT count(*) issupport FROM support WHERE COMMENT.id = support.targetid AND userid = :userid AND targettype = 101) issupport,
          (SELECT count(*) isfavorite FROM favorite WHERE COMMENT.id = favorite.targetid AND userid = :userid AND targettype = 101) isfavorite,
          (SELECT COUNT(*) FROM support WHERE targettype = 101 AND targetid = COMMENT.id) supportcount,
          (SELECT COUNT(*) FROM favorite WHERE targettype = 101 AND targetid = COMMENT.id) favoritecount,
          (SELECT username from user WHERE COMMENT.userid = user.id) username,
          (SELECT headimg from user WHERE COMMENT.userid = user.id) headimg
          FROM COMMENT 
          WHERE pid = :id
          UNION ALL
          SELECT c.id, c.pid, c.content,c.createtime, c.userid,
          IF(c.userid=:userid,1,0) isyour,
          (SELECT count(*) issupport FROM support WHERE c.id = support.targetid AND userid = :userid AND targettype = 101) issupport,
          (SELECT count(*) isfavorite FROM favorite WHERE c.id = favorite.targetid AND userid = :userid AND targettype = 101) isfavorite,
          (SELECT COUNT(*) FROM support WHERE targettype = 101 AND targetid = c.id) supportcount,
          (SELECT COUNT(*) FROM favorite WHERE targettype = 101 AND targetid = c.id) favoritecount,
          (SELECT username from user WHERE c.userid = user.id) username,
          (SELECT headimg from user WHERE c.userid = user.id) headimg
          FROM COMMENT c
          INNER JOIN folder_recursion fr ON c.pid = fr.id 
          ) SELECT *  FROM folder_recursion ORDER BY createtime desc;
        `;
    let [results_son] = await Model.sequelize.query(sql, {
      replacements: {
        userid: params.userid ?? null,
        id: params.id,
      },
    });
    return results_son;

弹框消息列表详情,则弹框展示消息的回复列表:

image.png

在这里再次进行弹框回复,需要注意,这里的弹框回复不同于列表的弹框回复,因为列表的弹框回复的列表项是消息体,而这里的列表项则是评论体。

这是消息体:

image.png

这是评论回复体:

image.png

因为消息体是统一的接口返回的数据,并且对一些消息数据做过聚合处理,所以消息体我是没有记录id的,而是用ids作为id的连接字符串字段统一返回,所以对消息列表进行回复时都是借用的target和messagetarget。

所以对这里的回复,就需要重新去区分了,主要还是前端方面完成的业务逻辑,每次我获取某个评论或回复的回复时,都会记录一个activeComment字段,即当前列表项这个消息体这个主体:

image.png 这个消息体主要就是提供了targettype和targetid,得到targettype和targetid后,那么在消息详情列表中添加评论并发送消息的前端业务代码如下:

      addComment({
        pid: item.id,
        targetid: this.activeComment.messagetarget.targetid,
        content: content,
        targettype: this.activeComment.messagetarget.targettype
      }).then(res => {
        // this.showAllReply(piditem)
        // return console.log(res)
        this.$sendMessage({
            targettype:101,
            targetid:item.id,
            reuserid:item.userid,
            messagetype:3,//评论
            messageid:res.data.id,
          })
        // this.getComment()
      })

验证下功能,用谷歌浏览器在详情页面发送回复:

image.png

用edage浏览器查看另一个用户:

image.png

再去文章页面看看:

image.png

从数据业务逻辑上来看,是没有什么问题了,当然,优化的地方其实蛮多的,因为性能不是主要问题,主要是把业务流程弄清楚,所以其他基本问题都是前端的了。