Skip to content

12.4 消息与关注接口

消息中心的接口大多数是对已有集合的数据查询,但是需要添加一个消息的已读和未读逻辑。为了维护用户的消息状态,我们还要再新建一个消息集合。消息集合的字段如下:

  • user_id:接受消息的用户 ID。
  • source_id:评论或收藏或关注的 ID。
  • type:消息类型,1 表示评论,2 表示收藏和赞,3 表示关注。
  • status:状态,0 表示未读,1 表示已读。
  • created_at:创建时间。

新建文件 model/messages.js 文件,将上面的字段编写为模型,代码如下:

js
// model/messages.js
const mongoose = require("mongoose");
const { ObjectId } = mongoose.Types;

const messagesSchema = new mongoose.Schema({
  user_id: { type: ObjectId, required: true },
  source_id: { type: ObjectId, required: true },
  type: { type: Number, enum: [1, 2, 3], required: true },
  status: { type: Number, enum: [0, 1], default: 0, required: true },
  created_at: { type: Date, default: Date.now },
});

const Model = mongoose.model("messages", messagesSchema);

12.4.1 消息列表接口

继续创建 router/messages.js 文件并编写路由代码,然后写一个获取消息列表的接口。

js
router.get("/lists", async (req, res, next) => {
  let { user_id } = req.query;
  try {
    let result = await MessModel.aggregate([
      { $match: { user_id: ObjectId(user_id), status: 0 } },
      {
        $group: {
          _id: "$type",
          count: {
            $sum: 1,
          },
        },
      },
    ]);
    let rsinfo = Object.fromEntries(
      result.map((json) => ["type" + json._id, json.count])
    );
    let resjson = {
      comment: rsinfo["type1"] || 0,
      praise: rsinfo["type2"] || 0,
      follow: rsinfo["type3"] || 0,
      total: result.reduce((a, b) => a.count + b.count),
    };
    res.send(resjson);
  } catch (err) {
    next(err);
  }
});

上面代码中首次使用了 $group 管道阶段,主要用于分组统计查询。我们要分别统计评论、点赞和关注的未读数量,因此要分组统计。统计后再经过处理,将其组合为一个对象返回。

在路由配置中注册该路由如下:

js
// config/router.js
const messRouter = require('../router/messages.js')
const router = (app) => {
  ...
  app.use('/messages', messRouter)
};

然后我们测试该接口,运行结果如图所示:

图中的返回结果,准确统计了评论、点赞、关注的未读数量以及总的未读数量,满足我们展示消息的要求。

有了消息列表,还需要创建消息的接口吗?其实不需要,只要我们在创建评论、创建点赞/收藏、添加关注的接口中添加一下 MessModel.create() 方法,在这些接口调用成功后自动创建消息即可。

比如在创建评论之后插入消息,我们修改 router/comments.js 中的创建数据路由如下:

js
router.post('/create', async (req, res, next) => {
  let body = req.body
  try {
    let result = await CommsModel.create(body)
    await MessModel.create({
      source_id: result._id,
      type: 1,
      user_id: body.target_user,
    })
    res.send(result)
  } catch (err) {
    ...
  }
})

12.4.2 关注与取消关注接口

消息中心还有个关注者列表的展示,并且用户可以快速关注粉丝,页面如图所示:

因此我们需要一套用户关注的逻辑,这需要一个新的集合,不过结构简单。创建 model/follows.js 文件,编写关注集合的模型如下:

js
const { ObjectId } = mongoose.Types;
const followsSchema = new mongoose.Schema({
  user_id: { type: ObjectId, required: true }, // 用户 ID
  fans_id: { type: ObjectId, required: true }, // 粉丝 ID
  created_at: { type: Date, default: Date.now },
});

const Model = mongoose.model("follows", followsSchema);

只需要三个字段,用户 ID、粉丝 ID 和创建时间。然后编写路由文件 router/follows.js,在这里编写关注和取消关注的接口。

和点赞与取消点赞的逻辑类似,关注也不存在一个人对另一个人关注两次的情况,因此这里也非常适合将关注与取消关注放在一个接口里实现,根据查询结果自动执行创建或删除。

创建路由名为 “/toggle”,方法为 POST,代码如下:

js
// router/follows.js
var FollowsModel = require("../model/follows");
var MessModel = require("../model/messages");

router.post("/toggle", async (req, res, next) => {
  let body = req.body;
  try {
    let { user_id, fans_id } = body;
    if (!user_id || !fans_id) {
      return res.status(400).send({ message: "参数缺失" });
    }
    let action = "delete";
    let result = await FollowsModel.findOneAndDelete(body);
    if (!result) {
      action = "create";
      result = await FollowsModel.create(body);
      await MessModel.create({
        source_id: result._id,
        type: 3,
        user_id,
      });
    }
    res.send({
      action,
      message: action == "create" ? "关注成功" : "取消关注成功",
    });
  } catch (err) {
    next(err);
  }
});

代码中首先尝试删除操作(即取消关注),没有返回值表示没有找到数据,此时进入创建数据(关注)的逻辑。并且因为关注有消息通知,所以还要向消息集合添加数据。

接着我们注册该路由,关注和取消关注的接口就可以用了。

js
// config/router.js
const follRouter = require('../router/follows.js')
const router = app => {
  ...
  app.use('/follows', follRouter)
}

测试下执行结果,首先添加一个关注,如图所示。

相同参数再调用一次,可以看到关注已取消,如图所示。

12.4.3 关注者列表接口

有了关注和取消关注的功能,现在就可以获取某个用户的粉丝列表了。创建列表路由,代码如下:

js
router.get('/lists', async (req, res, next) => {
  let { user_id } = req.query
  try {
    let result = await FollowsModel.aggregate([
      {
        $match: {
          user_id: ObjectId(user_id),
        },
      },
      {
        $lookup: {
          from: 'users',
          localField: 'fans_id',
          foreignField: '_id',
          as: 'fans_info',
        },
      },
      {
        $lookup: {
          from: 'follows',
          let: {
            uid: '$user_id',
            fid: '$fans_id',
          },
          pipeline: [
            {
              $match: {
                $expr: {
                  $and: [
                    { $eq: ['$user_id', '$$fid'] },
                    { $eq: ['$fans_id', '$$uid'] },
                  ],
                },
              },
            },
          ],
          as: 'is_follow',
        },
      },
      {
        $addFields: {
          fans_info: {
            $first: '$fans_info',
          },
          is_follow: {
            $gt: [{ $size: '$is_follow' }, 0], // 判断数组长度大于0
          },
        },
      },
    ])
    res.send(result)
  } catch (err) {
    ...
  }
})

上面代码中,特别要介绍下第三条管道,它是 $lookup 管道操作符的高级用法,可以自定义关联筛选数据的细节,而不是默认将关联数据都列出来。新的两个属性含义如下:

  • let:设置自定义变量。
  • pipeline:设置关联查询时的管道操作细节。

我们将两个 follows 集合关联查询,使 user_id 与 fans_id 互相比较,从而查询出该粉丝是否被关注。测试列表接口,返回结果如下: