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

Node下用create插入ID数组创建Booking的Mongoose报错问题

问题描述

需要实现一个Booking文档关联多个Tour的功能,但在创建Booking时连续遇到两个错误:

  1. 初始错误:Booking validation failed: tour.0: Cast to [ObjectId] failed for value "[\"5c88fa8cf4afda39709c295a,5c88fa8cf4afda39709c2951\"]" (type string) at path "tour.0"
  2. 修改后新错误: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',
  });
});

问题根源

  1. 第一个错误:传递的tour是字符串形式的数组(如"[id1,id2]"),Mongoose无法将其直接转为ObjectId数组;
  2. 第二个错误: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]);
});

额外优化建议

  1. 简化Schema:tour字段可以简化为直接的ObjectId数组,无需嵌套对象,功能一致:
tour: {
  type: [mongoose.Schema.ObjectId],
  ref: 'Tour',
  required: [true, 'Booking must belong to Tours!'],
}
  1. 避免重复响应:后端中res.redirect已经发送了HTTP响应,之后不能再调用res.status(200).json(...),否则会触发错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 14:25:47