Mongoose更新商品库存报Cast to ObjectId failed错误排查
问题描述
尝试实现下单后自动扣减对应商品库存的逻辑:
- 前端在
PlaceOrderScreen.js中读取cart.cartItems数据,通过map遍历生成items数组,每个元素包含商品_id、原有countInStock值、下单数量qty、扣减后新库存值(计算规则为countInStock - qty) - 点击下单按钮触发
placeOrderHandler时,同时调用两个方法:createOrder生成订单、updateInstock将库存数据提交到后端 - 预期在
productRouter.js的对应接口接收数据,完成商品库存的批量更新
实际运行出现两个异常:
- 触发Mongoose报错:
Cast to ObjectId failed for value "updateInstock" (type string) at path "_id" for model "Product" - 后端接口内执行
console.log(req.body.items)无法在终端打印前端提交的库存数据,无法定位是路由配置还是请求传参环节的问题
相关代码
PlaceOrderScreen.js
import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import {createOrder} from '../actions/orderActions'; import {updateInstock} from '../actions/productActions'; import { ORDER_CREATE_RESET } from '../constants/orderConstants'; export default function PlaceOrderScreen(props) { const cart = useSelector((state) => state.cart); if (!cart.paymentMethod) { props.history.push('/payment'); } const orderCreate = useSelector((state) => state.orderCreate); const { loading, success, error, order } = orderCreate; const item = cart.cartItems const items = item.map(product => ({ _id: product._id, countInStock: product.countInStock, qty: product.qty, new:(product.countInStock - product.qty) })) const dispatch = useDispatch(); const placeOrderHandler = () => { dispatch(createOrder({ ...cart, orderItems: cart.cartItems })); dispatch(updateInstock(items)); }; useEffect(() => { if (success) { props.history.push(`/order/${order._id}`); dispatch({ type: ORDER_CREATE_RESET }); } }, [dispatch, order, props.history, success]); return ( <div> <div className="row top"> <div className="col-1"> <div className="card card-body"> <ul> <li> <div className="row"> ... </div> </li> <li> <button type="button" onClick={placeOrderHandler} className="primary block" disabled={cart.cartItems.length === 0} > Place Order </button> </li> </ul> </div> </div> </div> </div> ); }
productActions.js
import Axios from 'axios'; import { PRODUCT_UPDATEINSTOCK_REQUEST, PRODUCT_UPDATEINSTOCK_SUCCESS, PRODUCT_UPDATEINSTOCK_FAIL, } from '../constants/productConstants'; import { BASE_URL } from '../constants/app.constants'; export const updateInstock = (items) => async (dispatch, getState) => { dispatch({ type: PRODUCT_UPDATEINSTOCK_REQUEST, payload: items }); const { userSignin: { userInfo }, } = getState(); try { const { data } = await Axios.put(`${BASE_URL}/api/products/updateInstock`, items, { headers: { Authorization: `Bearer ${userInfo.token}` }, }); dispatch({ type: PRODUCT_UPDATEINSTOCK_SUCCESS, payload: data }); } catch (error) { const message = error.response && error.response.data.message ? error.response.data.message : error.message; dispatch({ type: PRODUCT_UPDATEINSTOCK_FAIL, error: message }); } };
productRouter.js
productRouter.put( '/updateInstock', expressAsyncHandler(async (req, res) => { console.log(req.body.items) }) );
问题根因&解决方法
一共两个核心问题,完全对应你遇到的报错:
路由顺序错误导致Mongoose类型转换报错
你的/updateInstock路由定义顺序在带路径参数的商品路由(格式类似productRouter.put('/:id', xxx))之后。Express是按路由定义顺序从上到下匹配的,当请求/api/products/updateInstock时,会先被/:id路由捕获,把字符串updateInstock当成商品_id去查询数据库,自然触发ObjectId类型转换失败的报错。解决:把
/updateInstock这类固定路径的路由,移动到所有带路径参数(比如/:id)的路由前面,保证固定路径优先匹配。传参取值不匹配导致后端拿不到数据
前端Axios发请求时,直接把items数组作为请求体发送,也就是说req.body本身就是你传的items数组,不是包含items属性的对象。你在后端写req.body.items相当于在数组上找items属性,拿到的自然是undefined,打印不出内容。解决:后端直接用
console.log(req.body)就能拿到前端传的库存数组,后续批量更新库存直接遍历这个数组操作即可。
额外提一个逻辑隐患:你现在把创建订单和扣减库存分成两个独立请求发送,很容易出现订单创建成功但库存扣减失败、或者库存扣了但订单没生成的数据一致性问题,更稳妥的做法是把扣减库存的逻辑放到后端创建订单的接口里,订单创建成功后直接在后端事务里完成库存扣减,不要前端分两次发请求。
内容的提问来源于stack exchange,提问作者Gigi101

