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 }); } };
代码说明
- 异步处理:使用
async/await简化异步流程,避免回调嵌套。 - 重复操作校验:通过
$ne判断用户未在对应数组中,防止重复点赞/点踩。 - 聚合管道更新:在
updateOne的第二个参数使用数组形式的聚合管道,先修改数组,再通过$size运算符获取数组长度并同步到计数器字段。 - 取消操作处理:先查询文档判断用户之前的操作类型,再执行对应的
$pull操作,最后同步计数器。
内容的提问来源于stack exchange,提问作者Elradsoldier
相关产品推荐
相关产品推荐

