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

如何在Node.js中通过Multer仅在CRUD成功时上传Cloudinary图片

确保学生数据操作成功后再上传头像到Cloudinary

我是一名学生,开发了一个CRUD项目,使用Multer处理学生头像并存储到Cloudinary中。我希望仅在学生数据创建或更新成功时,才将头像上传至Cloudinary。以下是我当前用于创建和更新学生信息的控制器函数:

当前创建学生函数

async function addNewStudent(req, res) {
     try {
     const { student_roll, student_name, student_email } = req.body;

    const existingStudentID = await Students.findOne({ student_roll });
    if (existingStudentID) {
        return res.status(400).json({ status: 400, message: `Student RollNo. '${student_roll}' already exists in the database.` });
    }

    const existingStudentEmail = await Students.findOne({ student_email });
    if (existingStudentEmail) {
        return res.status(400).json({ status: 400, message: `Student email '${student_email}' already exists in the database.` });
    }

    const result = await cloudinary.uploader.upload(req.file.path, { resource_type: 'image' });
    const newStudent = await Students.create({
        student_roll,
        student_name,
        student_email,
        profile_image: {
            public_id: result.public_id,
            secure_url: result.secure_url
        }
    });

    return actionCompleteResponse({ res, data: newStudent });
} catch (error) {
    console.error(error);
    return sendError({ res, error });
}}

当前更新学生函数

async function updateStudent(req, res) {
         try {
         const updates = req.body;

    if (!ObjectId.isValid(req.params.id)) {
        throw 'NOT_VALID_DOCUMENT_ID';
    }

    const existingStudentID = await Students.findOne({ student_roll: updates.student_roll });
    if (existingStudentID) {
        return res.status(400).json({ status: 400, message: `Student roll no. '${updates.student_roll}' already exists in the database.` });
    }

    const existingStudentName = await Students.findOne({ student_name: updates.student_name });
    if (existingStudentName) {
        return res.status(400).json({ status: 400, message: `Student name '${updates.student_name}' already exists in the database.` });
    }

    const existingStudentEmail = await Students.findOne({ student_email: updates.student_email });
    if (existingStudentEmail) {
        return res.status(400).json({ status: 400, message: `Student email '${updates.student_email}' already exists in the database.` });
    }

    if (req.file) {
        const existingStudentImage = await Students.findOne({ _id: new ObjectId(req.params.id) });
        const imageID = existingStudentImage.profile_image.public_id;

        if (existingStudentImage.profile_image && imageID) {
            const deletePreviousImage = await cloudinary.uploader.destroy(imageID);
        }

        const newImage = await cloudinary.uploader.upload(req.file.path);
        updates.profile_image = {
            public_id: newImage.public_id,
            secure_url: newImage.secure_url
        };
    }

    const updatedStudent = await Students.updateOne({ _id: new ObjectId(req.params.id) }, { $set: updates });
    return actionCompleteResponse({ res, data: updates });
} catch (error) {
    return sendError({ res, error });
}}

解决方案:调整操作顺序 + 错误回滚

一、修改创建学生函数

核心逻辑:先创建无头像的学生数据,确认数据写入成功后再上传头像,最后更新头像字段;若中间步骤失败,回滚已创建的资源。

async function addNewStudent(req, res) {
  try {
    const { student_roll, student_name, student_email } = req.body;

    // 唯一性校验
    const existingStudentID = await Students.findOne({ student_roll });
    if (existingStudentID) {
      return res.status(400).json({ status: 400, message: `Student RollNo. '${student_roll}' already exists in the database.` });
    }

    const existingStudentEmail = await Students.findOne({ student_email });
    if (existingStudentEmail) {
      return res.status(400).json({ status: 400, message: `Student email '${student_email}' already exists in the database.` });
    }

    // 1. 先创建不带头像的基础学生数据
    const newStudent = await Students.create({
      student_roll,
      student_name,
      student_email,
      profile_image: null
    });

    // 2. 数据创建成功后,上传头像到Cloudinary
    const result = await cloudinary.uploader.upload(req.file.path, { resource_type: 'image' });

    // 3. 更新学生数据的头像字段
    await Students.findByIdAndUpdate(newStudent._id, {
      profile_image: {
        public_id: result.public_id,
        secure_url: result.secure_url
      }
    });

    // 返回更新后的完整数据
    const updatedStudent = await Students.findById(newStudent._id);
    return actionCompleteResponse({ res, data: updatedStudent });
  } catch (error) {
    console.error(error);
    // 回滚:如果已创建学生但头像上传失败,删除该学生数据
    if (typeof newStudent !== 'undefined') {
      await Students.findByIdAndDelete(newStudent._id);
    }
    // 清理Multer本地临时文件
    if (req.file) {
      require('fs').unlinkSync(req.file.path);
    }
    return sendError({ res, error });
  }
}

二、修改更新学生函数

核心逻辑:先更新非头像字段,确认数据更新成功后再处理头像上传和旧头像删除;若中间步骤失败,回滚已上传的新头像。

async function updateStudent(req, res) {
  try {
    const updates = { ...req.body };
    const studentId = req.params.id;
    let newImage = null;

    if (!ObjectId.isValid(studentId)) {
      throw 'NOT_VALID_DOCUMENT_ID';
    }

    // 获取原学生数据,用于后续校验和旧头像处理
    const originalStudent = await Students.findById(studentId);
    if (!originalStudent) {
      return res.status(404).json({ status: 404, message: 'Student not found' });
    }

    // 唯一性校验:排除当前学生自身的记录
    const existingStudentID = await Students.findOne({ 
      student_roll: updates.student_roll, 
      _id: { $ne: studentId } 
    });
    if (existingStudentID) {
      return res.status(400).json({ status: 400, message: `Student roll no. '${updates.student_roll}' already exists in the database.` });
    }

    const existingStudentEmail = await Students.findOne({ 
      student_email: updates.student_email, 
      _id: { $ne: studentId } 
    });
    if (existingStudentEmail) {
      return res.status(400).json({ status: 400, message: `Student email '${updates.student_email}' already exists in the database.` });
    }

    // 1. 先更新非头像字段
    const updateFields = { ...updates };
    delete updateFields.profile_image;
    let updatedStudent = await Students.findByIdAndUpdate(
      studentId, 
      { $set: updateFields }, 
      { new: true }
    );

    // 2. 数据更新成功后,处理头像(如果有新头像)
    if (req.file) {
      // 上传新头像
      newImage = await cloudinary.uploader.upload(req.file.path);
      // 更新头像字段
      updatedStudent = await Students.findByIdAndUpdate(
        studentId,
        {
          profile_image: {
            public_id: newImage.public_id,
            secure_url: newImage.secure_url
          }
        },
        { new: true }
      );
      // 删除旧头像
      if (originalStudent.profile_image?.public_id) {
        await cloudinary.uploader.destroy(originalStudent.profile_image.public_id);
      }
    }

    return actionCompleteResponse({ res, data: updatedStudent });
  } catch (error) {
    console.error(error);
    // 回滚:如果已上传新头像但数据更新失败,删除新头像
    if (newImage?.public_id) {
      await cloudinary.uploader.destroy(newImage.public_id);
    }
    // 清理Multer本地临时文件
    if (req.file) {
      require('fs').unlinkSync(req.file.path);
    }
    return sendError({ res, error });
  }
}

关键优化点

  1. 操作顺序反转:将数据写入/更新放在头像操作之前,确保只有数据操作成功才触发头像上传。
  2. 错误回滚机制:在catch块中清理已创建的学生数据、已上传的头像、Multer本地临时文件,避免产生垃圾资源。
  3. 唯一性校验修正:更新时排除当前学生自身记录,避免因修改自身信息触发重复校验报错。
  4. 移除不合理校验:删除了学生姓名的唯一性校验(同名学生是合理场景)。

内容的提问来源于stack exchange,提问作者Jaskaran Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 04:58:10