当平台有其他用户的行为影响到我们时,我们就会接收到平台的提醒消息,这就是站内消息系统的核心业务。
消息大体可以分为两类,一是用户产生的消息,二是系统产生的消息。
先说用户产生的消息,比如用户点赞、收藏、评论某个主体时,就会触发当前主体的发起者。
这个我们暂时借鉴下掘金的,以下是掘金中的消息回馈数据:
dst_info是目标源,也就是当前的操作体,比如点赞、评论或回复的内容体,是文章、评论还是回复等等。
需要返回的基本字段有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"],
});
},
返回的结构如下:
前端针对该数据做一次封装,总量数据有多少,评论回复数据有多少,点赞或收藏的消息有多少:
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
}
})
}
})
},
那么展示效果就是这样了:
这个数据目前只是后台增加的静态数据,需要动态构造,那么就在用户点赞或收藏时,添加动态消息的功能。
后台的逻辑代码,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
})
收藏也是一样的,那么被通知的用户在站内就会有红点消息提示,这个数据提示就是真实的消息提示了:
如何去处理列表呢?
其实按照掘金的那种思路,我们其实就去拿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(",")})
前端把这个列表拿到并展示列表即可:
那什么时候修改为已读呢?
实际上,应该在拉取数据时,就自动将这些数据列表做已读处理,即在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,我这里再用另外一个账号添加三条点赞和收藏数据:
那么用户下次再进入消息通知也,看到的就是聚合后的数据了,并且时间更新为用户最新点赞和收藏的时间,即:
前端的逻辑其实挺简单的:
<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()
})
发布评论后,用户将在通知页面得到提示:
根据前面拉取消息列表的逻辑规则,拉取过的数据将会修改为已读状态,那么回复数据被拉取后,数据库数据就变更为已读了:
所以这里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。
基本功能完成了,现在验证该功能,在用户【月亮】发布的文章下发布一条评论:
发布评论时并提交通知,编写文章者将收到一条消息:
这里就关注两个点,一个是target,这是所有消息都必有的东西,这代表动作触发的目标,即评论这篇文章的,所以target就指向这篇文章。
第二个是messagetype,只有当messagetype为3时,才会携带的参数,这代表当前消息的回复内容,也就是发送消息的这条评论本身。
回复这条评论:
再回到之前那个页面,可以查看到当前评论的回复:
并且用户也接收到了该消息:
进入消息页面之后,也能查询到该消息:
那么业务逻辑这块儿基本就没什么大问题了,所以接下来就是处理消息列表,消息列表可以查看到某条评论的列表数据,这里不能通过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;
弹框消息列表详情,则弹框展示消息的回复列表:
在这里再次进行弹框回复,需要注意,这里的弹框回复不同于列表的弹框回复,因为列表的弹框回复的列表项是消息体,而这里的列表项则是评论体。
这是消息体:
这是评论回复体:
因为消息体是统一的接口返回的数据,并且对一些消息数据做过聚合处理,所以消息体我是没有记录id的,而是用ids作为id的连接字符串字段统一返回,所以对消息列表进行回复时都是借用的target和messagetarget。
所以对这里的回复,就需要重新去区分了,主要还是前端方面完成的业务逻辑,每次我获取某个评论或回复的回复时,都会记录一个activeComment字段,即当前列表项这个消息体这个主体:
这个消息体主要就是提供了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()
})
验证下功能,用谷歌浏览器在详情页面发送回复:
用edage浏览器查看另一个用户:
再去文章页面看看:
从数据业务逻辑上来看,是没有什么问题了,当然,优化的地方其实蛮多的,因为性能不是主要问题,主要是把业务流程弄清楚,所以其他基本问题都是前端的了。