You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose/Node.js如何获取数组长度并用作计数器值?

点赞/点踩系统:基于数组长度同步计数器字段的问题

背景说明

使用Mongoose定义的酱料模型如下,usersLiked/usersDisliked数组存储点赞/点踩用户ID,likes/dislikes为对应的计数器字段:

const mongoose = require('mongoose');
const sauceSchema = mongoose.Schema({
  userId: {type:String},
  name: {type:String, required:true},
  manufacturer:{type:String, required:true},
  description:{type:String, required:true},
  mainPepper: {type:String, required:true},
  imageUrl:{type:String, required:true},
  heat: {type:Number, required:true},
  likes: {type:Number},
  dislikes : {type:Number},
  usersLiked : {type:["String <userId>"]},
  usersDisliked: {type:["String <userId>"]},
});

module.exports = mongoose.model('sauce', sauceSchema);

问题描述

需要编写点赞/点踩的控制器路由,要求通过usersLiked和usersDisliked数组的长度同步更新likes和dislikes计数器字段,但尝试多种方案后要么返回undefined,要么接口无响应。

已尝试方案

仅保留点赞逻辑的路由代码:

exports.likeSauce = (req,res,next) =>{  
  const liker = req.body.userId;
  let likeStatus = req.body.like;

  if(likeStatus === 1){
    sauce.updateOne({_id:req.params.id}, {$push:{usersLiked:liker}, $inc:{likes:totalLiked}})
      .then(()=>res.status(201).json({message:'you liked this sauce'}))
      .catch(error=>res.status(400).json({error}))
 }else{}

尝试获取数组长度的两种无效写法:

// 写法1
let totalLiked = sauce.findOne([{$match:{_id:req.params.id}}, {$project:{usersLiked:{$size:'$usersLiked'}}}])
// 写法2
let totalLiked = sauce.aggregate([{$match:{_id:req.params.id,usersLiked:{$size:'$usersLiked'}}}])

核心错误:findOne和aggregate都是异步操作,直接赋值会得到Promise对象而非实际数值,且更新操作无法直接使用外部未解析的Promise值。

解决方案

核心思路

利用MongoDB 4.2+支持的聚合管道式更新,在单次更新操作中完成数组修改和计数器同步,无需单独查询数组长度;同时添加用户重复操作的校验(避免重复点赞/点踩)。

完整控制器代码

exports.likeSauce = async (req, res, next) => {
  const { userId: liker, like: likeStatus } = req.body;
  const sauceId = req.params.id;

  try {
    switch (likeStatus) {
      // 点赞操作
      case 1:
        await sauce.updateOne(
          { _id: sauceId, usersLiked: { $ne: liker }, usersDisliked: { $ne: liker } },
          [
            { $push: { usersLiked: liker } },
            { $set: { likes: { $size: "$usersLiked" }, dislikes: { $size: "$usersDisliked" } } }
          ]
        );
        res.status(201).json({ message: '点赞成功' });
        break;

      // 取消点赞/点踩操作
      case 0:
        const sauceDoc = await sauce.findById(sauceId);
        let updateOps = [];
        if (sauceDoc.usersLiked.includes(liker)) {
          updateOps.push({ $pull: { usersLiked: liker } });
        } else if (sauceDoc.usersDisliked.includes(liker)) {
          updateOps.push({ $pull: { usersDisliked: liker } });
        }
        updateOps.push({ 
          $set: { 
            likes: { $size: "$usersLiked" }, 
            dislikes: { $size: "$usersDisliked" } 
          } 
        });
        await sauce.updateOne({ _id: sauceId }, updateOps);
        res.status(200).json({ message: '取消操作成功' });
        break;

      // 点踩操作
      case -1:
        await sauce.updateOne(
          { _id: sauceId, usersDisliked: { $ne: liker }, usersLiked: { $ne: liker } },
          [
            { $push: { usersDisliked: liker } },
            { $set: { likes: { $size: "$usersLiked" }, dislikes: { $size: "$usersDisliked" } } }
          ]
        );
        res.status(201).json({ message: '点踩成功' });
        break;

      default:
        res.status(400).json({ error: '无效的操作状态' });
    }
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

代码说明

  1. 异步处理:使用async/await简化异步流程,避免回调嵌套。
  2. 重复操作校验:通过$ne判断用户未在对应数组中,防止重复点赞/点踩。
  3. 聚合管道更新:在updateOne的第二个参数使用数组形式的聚合管道,先修改数组,再通过$size运算符获取数组长度并同步到计数器字段。
  4. 取消操作处理:先查询文档判断用户之前的操作类型,再执行对应的$pull操作,最后同步计数器。

内容的提问来源于stack exchange,提问作者Elradsoldier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 01:45:29