Node.js+MongoDB购物车同ID多尺码商品购买更新报错
问题场景
商品模块支持尺码选择,同一款商品选择不同尺码加入购物车时会生成独立购物车条目,效果如下:
业务要求:用户结算购物车成功后,本次已购买的对应尺码需要从商品详情页的可选尺码列表中移除。
现有逻辑问题:
- 购买2款不同ID商品时功能正常
- 购买同ID、不同尺码的商品时,仅第一个尺码会被成功过滤,抛出版本错误:
(node:21336) UnhandledPromiseRejectionWarning: VersionError: No matching document found for id "62bee4ce92e7c57a195686ae" version 0 modifiedPaths "size"
- 调整代码后出现新报错:
product.save is not a function
问题根因
- 版本错误原因:原代码使用
forEach搭配async回调时,所有异步数据库操作是并行执行的。遇到同商品ID不同尺码的场景,多个并行任务会同时查询到版本号为0的同一份商品文档,第一个任务save成功后文档版本号会自增为1,后续任务拿着旧版本0的文档执行save,就会触发MongoDB的版本校验冲突,抛出匹配不到对应版本文档的错误。另外forEach不会等待异步回调执行完成,会出现商品数据还没更新完就已经生成订单返回的不一致问题。 - save不存在报错原因:
Product.find()返回的是匹配到的文档数组,数组对象本身没有save方法,直接对查询结果数组调用save()必然报错。同时调整后的代码用map遍历修改数组内文档时,没有对单个文档执行保存,逻辑本身也不成立。
修复方案
核心修复思路:先聚合每个商品需要移除的已购尺码,对同一个商品只执行一次查询、一次保存,从根源避免同文档并行更新的版本冲突,同时保证所有数据库操作按顺序执行完成后再生成订单。
修复后完整代码
createOrder: async (_, {}, context) => { const userAuth = await auth(context); const cart = await Cart.findOne({ userId: userAuth._id }); if (!cart) { throw new UserInputError('No available order!'); } // 聚合每个商品ID对应的已购尺码,同商品多尺码合并处理 const productPurchasedSizeMap = new Map(); cart.cartProducts.forEach(item => { const pid = item.productId.toString(); const size = +item.size; if (!productPurchasedSizeMap.has(pid)) { productPurchasedSizeMap.set(pid, new Set()); } productPurchasedSizeMap.get(pid).add(size); }); // 逐商品更新可选尺码,单次查询+单次保存避免版本冲突 for (const [pid, sizesToRemove] of productPurchasedSizeMap) { const product = await Product.findById(pid); if (product) { product.size = product.size.filter(stockSize => !sizesToRemove.has(stockSize)); await product.save(); } } const newOrder = new Order({ orderProducts: cart.cartProducts, purchasedBy: userAuth._id, datePurchased: new Date().toISOString(), }); await newOrder.save(); // 可选:下单成功后清空已结算的购物车内容 // cart.cartProducts = []; // await cart.save(); return newOrder; },
高性能批量更新写法(可选)
如果商品量级大,可以用MongoDB的bulkWrite做批量更新,省去逐个查询文档的步骤,性能更高,也不会出现版本冲突问题:
// 替换上面逐商品更新的for循环即可 const bulkUpdateTasks = []; for (const [pid, sizesToRemove] of productPurchasedSizeMap) { bulkUpdateTasks.push({ updateOne: { filter: { _id: pid }, // $pullAll 直接从数组中移除所有传入的尺码值 update: { $pullAll: { size: Array.from(sizesToRemove) } } } }); } if (bulkUpdateTasks.length) { await Product.bulkWrite(bulkUpdateTasks); }
内容的提问来源于stack exchange,提问作者עמית שוקרון
相关产品推荐
相关产品推荐

