如何通过Multer处理多文件FormData并关联MongoDB存储试题
考试应用多试题带图片上传解决方案
问题背景
我正在开发一款考试应用,技术栈为Express.js、React.js、Multer、MongoDB,核心需求是支持用户添加可能包含图片的多道试题。目前卡在两个关键点:
- 前端如何用FormData格式传递多试题数据?比如
question1Data: "xxx"、question1Image: file这种结构是否合理?(我负责Node.js后端,同事负责React.js前端) - 后端如何通过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}` } }); };
后端处理流程优化
结合你的现有代码,需要调整逻辑以实现图片与试题的正确关联:
- Multer接收图片数组:通过
upload.array('questionImages')接收所有上传的图片; - 解析试题JSON数据:从
req.body.questions解析出试题数组; - 关联图片与试题:根据前端传递的
imageIndex,将Multer返回的图片信息(路径、文件名等)绑定到对应试题; - 存储图片元数据:调用
FileServices.addFile保存图片信息,获取图片_id; - 批量创建试题:将关联了图片
_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
相关产品推荐
相关产品推荐

