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

如何通过Multer处理多文件FormData并关联MongoDB存储试题

考试应用多试题带图片上传解决方案

问题背景

我正在开发一款考试应用,技术栈为Express.js、React.js、Multer、MongoDB,核心需求是支持用户添加可能包含图片的多道试题。目前卡在两个关键点:

  1. 前端如何用FormData格式传递多试题数据?比如question1Data: "xxx"、question1Image: file这种结构是否合理?(我负责Node.js后端,同事负责React.js前端)
  2. 后端如何通过Multer将每道题的图片存入服务器文件夹,获取图片路径后存入MongoDB,拿到文件_id再关联对应试题的其他字段,最终批量把多道题存入MongoDB?

前端FormData传递方案

不推荐用question1Data、question1Image这种带序号的字段名,维护成本高。更合理的方案是:

  • 将试题文本数据转为JSON字符串,用单个字段(比如questions)传递;
  • 图片统一用数组字段(比如questionImages)上传,同时在JSON中给带图片的试题标记对应的图片索引。

示例React代码

// 假设questions是前端维护的试题数组,每个item包含text、image(File对象,可选)等字段
const handleSubmit = async () => {
  const formData = new FormData();
  // 收集图片并给试题标记对应图片索引
  const questionData = questions.map((q, idx) => {
    if (q.image) {
      formData.append('questionImages', q.image);
      return { ...q, imageIndex: idx }; // 用索引关联图片和试题
    }
    return q;
  });
  // 把试题数据转成JSON字符串存入FormData
  formData.append('questions', JSON.stringify(questionData));
  
  // 发送请求
  await fetch('/api/addQuestions', {
    method: 'POST',
    body: formData,
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
};

后端处理流程优化

结合你的现有代码,需要调整逻辑以实现图片与试题的正确关联:

  1. Multer接收图片数组:通过upload.array('questionImages')接收所有上传的图片;
  2. 解析试题JSON数据:从req.body.questions解析出试题数组;
  3. 关联图片与试题:根据前端传递的imageIndex,将Multer返回的图片信息(路径、文件名等)绑定到对应试题;
  4. 存储图片元数据:调用FileServices.addFile保存图片信息,获取图片_id;
  5. 批量创建试题:将关联了图片_id的试题批量存入MongoDB。

现有代码调整建议

路由调整

修改Multer调用,指定接收图片的数组字段:

router.post("/addQuestions", authTeacherAdminSuperAdmin('Questions.Create'), uploadFile({ multipleImages: 'questionImages' }), QuestionController.addQuestions);

Multer中间件简化

针对多图片场景简化分支逻辑:

function uploadFile(options) {
  const storage = multer.diskStorage({
    destination: (req, file, cb) => {
      const folderPath = `./public/uploads/${req.user.orgId}/${req.user.id}/${req.dynamicFolder}`;
      createDynamicFolders(folderPath);
      cb(null, folderPath);
    },
    filename: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      const uniqueFilename = `${Date.now()}${ext}`;
      cb(null, uniqueFilename);
    }
  });

  const upload = multer({ storage: storage });

  return (req, res, next) => {
    upload.array(options.multipleImages)(req, res, (err) => {
      if (err) {
        console.error('文件上传错误:', err);
        return res.status(500).json({ message: '文件上传失败: ' + err.message });
      }
      next();
    });
  };
}

创建函数调整

修改create函数以实现图片与试题的关联:

static create = async (reqData) => {
  try {
    // 解析前端传递的试题JSON
    let questions = JSON.parse(reqData.questions);
    // 获取Multer上传的图片列表
    const uploadedImages = reqData.files || [];

    // 先整理需要保存的图片数据
    const imageTasks = questions
      .filter(q => q.imageIndex !== undefined)
      .map(q => {
        const imageFile = uploadedImages[q.imageIndex];
        return {
          path: imageFile.path,
          filename: imageFile.filename,
          originalName: imageFile.originalname,
          size: imageFile.size,
          mimetype: imageFile.mimetype,
          organizationId: reqData.organizationId,
          createdBy: reqData.createdBy
        };
      });

    // 批量保存图片,获取所有图片的_id
    const imageResults = await Promise.all(imageTasks.map(img => FileServices.addFile(img)));

    // 将图片_id关联回对应试题
    let imageIdx = 0;
    questions = questions.map(question => {
      const baseFields = {
        ...question,
        organizationId: reqData.organizationId,
        deleted: reqData.deleted || false,
        createdBy: reqData.createdBy
      };

      if (question.imageIndex !== undefined) {
        baseFields.image = imageResults[imageIdx]._id;
        imageIdx++;
        // 删除临时字段
        delete baseFields.imageIndex;
      } else {
        delete baseFields.image;
      }

      return baseFields;
    });

    // 批量创建试题
    return questions.length > 0 ? await Promise.all(questions.map(item => QuestionModel.create(item))) : [];
  } catch (error) {
    throw error;
  }
}

你提供的现有代码

路由代码

router.post("/addQuestions", authTeacherAdminSuperAdmin('Questions.Create'), uploadFile({ multipleImages: 'image' }), QuestionController.addQuestions);

Multer中间件代码

const multer = require('multer');
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const { array } = require('joi');

function createDynamicFolders(folderPath) {
    if (!fs.existsSync(folderPath)) {
        fs.mkdirSync(folderPath, { recursive: true });
    }
}


function uploadFile(options) {
    const storage = multer.diskStorage({
        destination: (req, file, cb) => {
            console.log('Dynamic folder: ', req.dynamicFolder);
            const folderPath = `./public/uploads/${req.user.orgId}/${req.user.id}/${req.dynamicFolder}`;
            createDynamicFolders(folderPath);
            cb(null, folderPath);
        },
        filename: (req, file, cb) => {
            const ext = path.extname(file.originalname);
            const uniqueFilename = `${Date.now()}${ext}`;
            cb(null, uniqueFilename);
        }
    });

    const upload = multer({ storage: storage });

    return function (req, res, next) {
        console.log(options, "options")
        console.log(options, "options")
        console.log('Uploading file for field:', options.fieldName);
        if (Array.isArray(options)) {
            upload.fields(options)(req, res, async function (err) {
                if (err) {
                    console.error('Error during file upload:', err);
                    return res.status(500).json({ message: 'File Upload Error: ' + err.message });
                }
                next();
            });
        } else if (options.fieldName) {
            upload.single(options.fieldName)(req, res, async function (err) {
                if (err) {
                    console.error('Error during file upload:', err);
                    return res.status(500).json({ message: 'File Upload Error: ' + err.message });
                }
                // // Check if the uploaded file is an image (you might want to add more validation)
                // if (req.file.mimetype.startsWith('image')) {
                //     // Resize the image using Sharp
                //     const resizedImagePath = `./public/uploads/${req.user.orgId}/${req.user.id}/${req.dynamicFolder}/resized_${req.file.filename}`;

                //     await sharp(req.file.path)
                //         .resize(300, 200, { fit: 'inside' })
                //         .toFile(resizedImagePath);

                //     // You can save the resized image path or perform further actions with it
                //     req.resizedImagePath = resizedImagePath;
                // }
                next();
            });

        } else {

            upload.array(options.multipleImages)(req, res, async function (err) {
                if (err) {
                    console.error('Error during file upload:', err);
                    return res.status(500).json({ message: 'File Upload Error: ' + err.message });
                }
                req.files = req.files.map((item, index) => ({ ...item, index }));
                next();
            });

        }


    };
}

module.exports = uploadFile;

创建函数代码

static create = async (data) => {
    try {
      let questions = []
      for (let index = 0; index < data.questions.length; index++) {
        const question = data.questions[index];
        if (question.image && Object.keys(question.image).length > 0) {

          const addImage = await FileServices.addFile(question.image);
          question.image = addImage._id
        } else {
          delete question.image
        }
        questions.push(question)
      }

      const result = {
        ...data,
        questions: questions.map((question) => ({
          ...question,
          organizationId: data.organizationId,
          deleted: data.deleted ? data.deleted : false,
          createdBy: data.createdBy
        }))
      };


      if (result.questions.length > 0) {
        const promises = result.questions.map(item => QuestionModel.create(item));
        return await Promise.all(promises)
      }
    } catch (error) {
      throw error;
    }
  }

内容的提问来源于stack exchange,提问作者Ridham Panchal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 16:02:03