Node下用create插入ID数组创建Booking的Mongoose报错问题
问题描述
需要实现一个Booking文档关联多个Tour的功能,但在创建Booking时连续遇到两个错误:
- 初始错误:
Booking validation failed: tour.0: Cast to [ObjectId] failed for value "[\"5c88fa8cf4afda39709c295a,5c88fa8cf4afda39709c2951\"]" (type string) at path "tour.0" - 修改后新错误:
Unexpected token u in JSON at position 0
相关代码
Mongoose Schema定义
const bookingSchema = new mongoose.Schema({ tour: [ { type: mongoose.Schema.ObjectId, ref: 'Tour', required: [true, 'Booking must belong to Tours!'], }, ], user: { type: mongoose.Schema.ObjectId, ref: 'User', required: [true, 'Booking must belong to User!'], }, price: { type: Number, required: [true, 'Booking must have a price'], }, createdAt: { type: Date, default: Date.now(), }, paid: { type: Boolean, default: true, }, }); bookingSchema.pre(/^find/, function (next) { this.populate('user').populate({ path: 'tour', select: 'name', }); });
前端Angular请求代码
createBookingCheckout(params: any): Observable<any> { console.log('serv'); return this.http.get<any>(`${CREATE_BOOKING_CHECKOUT}`, { params, withCredentials: true, }); }
后端Node.js创建Booking代码
exports.createBookingCheckout = catchAsync(async (req, res, next) => { const { order, tour: strTour } = req.query; const user = req.user._id; const parsedOrder = JSON.parse(order); const tour = JSON.parse(strTour); console.log(tour); let price = 0; parsedOrder.forEach( (obj) => (price = price + obj.price_data.unit_amount * obj.quantity) ); if (!parsedOrder && !user) return next(); await Booking.create({ tour, user, price }); res.redirect(req.originalUrl.split('?')[0]); res.status(200).json({ status: 'success', }); });
问题根源
- 第一个错误:传递的
tour是字符串形式的数组(如"[id1,id2]"),Mongoose无法将其直接转为ObjectId数组; - 第二个错误:
JSON.parse的参数为undefined,说明前端传递的tour/order参数未正确到达后端,或者参数值为空时直接调用了解析方法。
解决方案
步骤1:修正前端参数传递
Angular的HttpParams处理数组有两种可靠方式:
方式A:直接传递数组(推荐)
让HttpParams自动处理数组为多参数形式(tour=id1&tour=id2):
createBookingCheckout(params: any): Observable<any> { const httpParams = new HttpParams({ fromObject: params }); return this.http.get<any>(`${CREATE_BOOKING_CHECKOUT}`, { params: httpParams, withCredentials: true, }); }
方式B:手动序列化为JSON字符串
如果必须传递JSON格式,提前将数组转为字符串:
createBookingCheckout(params: any): Observable<any> { const formattedParams = { ...params, tour: JSON.stringify(params.tour), order: JSON.stringify(params.order) }; return this.http.get<any>(`${CREATE_BOOKING_CHECKOUT}`, { params: formattedParams, withCredentials: true, }); }
步骤2:修正后端解析逻辑
根据前端传递方式,调整后端代码:
对应方式A(数组参数)
exports.createBookingCheckout = catchAsync(async (req, res, next) => { const { order, tour } = req.query; const user = req.user._id; // 统一处理tour参数为数组 const tourIds = Array.isArray(tour) ? tour : [tour]; // 验证每个ID是否为有效ObjectId tourIds.forEach(id => { if (!mongoose.Types.ObjectId.isValid(id)) { return next(new AppError('无效的Tour ID', 400)); } }); // 安全解析order参数 let parsedOrder; if (order) { try { parsedOrder = JSON.parse(order); } catch (err) { return next(new AppError('订单数据格式错误', 400)); } } else { return next(new AppError('缺少订单数据', 400)); } // 计算总价 let price = 0; parsedOrder.forEach(obj => { price += obj.price_data.unit_amount * obj.quantity; }); if (!user) return next(new AppError('用户未认证', 401)); // 创建Booking await Booking.create({ tour: tourIds, user, price }); // 注意:redirect后不能再发送JSON响应,否则会报错 res.redirect(req.originalUrl.split('?')[0]); });
对应方式B(JSON字符串)
exports.createBookingCheckout = catchAsync(async (req, res, next) => { const { order, tour: strTour } = req.query; const user = req.user._id; // 安全解析tour参数 let tourIds; if (strTour) { try { tourIds = JSON.parse(strTour); if (!Array.isArray(tourIds)) { return next(new AppError('Tour必须是ID数组', 400)); } // 验证ID有效性 tourIds.forEach(id => { if (!mongoose.Types.ObjectId.isValid(id)) { return next(new AppError(`无效的Tour ID:${id}`, 400)); } }); } catch (err) { return next(new AppError('Tour数据格式错误', 400)); } } else { return next(new AppError('缺少Tour ID', 400)); } // 安全解析order参数 let parsedOrder; if (order) { try { parsedOrder = JSON.parse(order); } catch (err) { return next(new AppError('订单数据格式错误', 400)); } } else { return next(new AppError('缺少订单数据', 400)); } // 计算总价 let price = 0; parsedOrder.forEach(obj => { price += obj.price_data.unit_amount * obj.quantity; }); if (!user) return next(new AppError('用户未认证', 401)); // 创建Booking await Booking.create({ tour: tourIds, user, price }); // 避免重复响应 res.redirect(req.originalUrl.split('?')[0]); });
额外优化建议
- 简化Schema:tour字段可以简化为直接的ObjectId数组,无需嵌套对象,功能一致:
tour: { type: [mongoose.Schema.ObjectId], ref: 'Tour', required: [true, 'Booking must belong to Tours!'], }
- 避免重复响应:后端中
res.redirect已经发送了HTTP响应,之后不能再调用res.status(200).json(...),否则会触发错误。
内容的提问来源于stack exchange,提问作者Alex Karamanis
相关产品推荐
相关产品推荐

