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

Multer批量上传偶发失败无报错,异步逻辑问题排查求助

批量文件上传至数据库偶发异常问题排查与修复

问题描述

我开发了一个使用Multer中间件批量将文件存储到数据库的应用,整体配置完成且多数场景运行正常,但偶尔出现上传异常:文件已上传至服务器,但未保存到数据库,且无任何错误提示。怀疑是数据库连接延迟等问题,尝试将POST请求改为异步处理,但不确定异步逻辑是否正确。期望实现所有上传请求成功执行,或出现问题时生成明确的错误提示。

前端代码

import React, { useState } from 'react'
import BackButton from '../components/BackButton';
import Spinner from '../components/Spinner';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';

export const CreatePhoto = () => {

  const [category, setCategory] = useState('Reception');
  const [files, setFiles] = useState([]);
  const [loading, setLoading] = useState(false);
  const navigate = useNavigate(); 

  const handleChange = (e) => {
    setCategory(e.target.value);
  }

  const handlePhoto = (e) => {
    setFiles(Array.from(e.target.files))
  }

  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    if (!files.length) {
      alert('请选择要上传的图片');
      return;
    }
    const formData = new FormData();
    formData.append('category', category);
    files.forEach(file => {
      formData.append('photo', file);
    });
    setLoading(true);
    axios
      .post('http://localhost:5555/uploadImage', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      })
      .then(() => {
        setLoading(false);
        alert('上传成功');
        navigate('/');
      })
      .catch((error) => {
        setLoading(false);
        alert(`上传失败:${error.response?.data?.error || '未知错误'}`);
        console.error(error);
      });
  };

  return (
    <div className='p-4'>
      <BackButton />
      <h1 className='text-3xl my-4'>Upload Photos</h1>
      {loading ? <Spinner /> : ''}

      <div className='flex flex-col border-2 border-sky-400 rounded-xl w-[600px] p-4 mx-auto'>
        <form onSubmit={handleSubmit} encType='multipart/form-data'>
          <div className='my-4'>
            <label className='mb-2 inline-block text-neutral-500 dark:text-gray-800' htmlFor='category'>Category</label>
              <select id="category" name="category"
              className='bg-gray-50 border-2 border-yellow-600 text-gray-900 text-base rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-100 dark:border-yellow-500 dark:placeholder-gray-400 dark:text-gray-800 dark:focus:ring-blue-500 dark:focus:border-blue-500'
              value={category}
              onChange={handleChange}>
                <option value="Reception">Choose a Category</option>
                <option value="Traditional">Traditional</option>
                <option value="Court">Court</option>
                <option value="Reception">Reception</option>
              </select>
          </div>

          <div className='mb-3'>
            <label
              htmlFor="formFileMultiple"
              className='mb-2 inline-block text-neutral-500 dark:text-gray-800'
              >Upload Images</label>
            <input
              className='relative m-0 block w-full min-w-0 flex-auto cursor-pointer rounded border border-2 border-yellow-500 bg-gray-100 bg-clip-padding px-3 py-[0.32rem] text-base font-normal text-surface transition duration-300 ease-in-out file:-mx-3 file:-my-[0.32rem] file:me-3 file:cursor-pointer file:overflow-hidden file:rounded-none file:border-0 file:border-e file:border-solid file:border-inherit file:bg-transparent file:px-3  file:py-[0.32rem] file:text-surface focus:border-primary focus:text-gray-700 focus:shadow-inset focus:outline-none dark:border-yellow dark:text-black  file:dark:text-black'
              type="file"
              name="photo"
              onChange={handlePhoto}
              id="formFileMultiple"
              multiple="multiple" />
          </div>
          <button className='p-2 bg-sky-300 m-8' type='submit'>
            Upload
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreatePhoto

后端代码(原代码)

// ... 省略其他配置
const fileStorage = multer.diskStorage({
    destination: 'images',
    filename : (request, file, cb) => {
        cb(null, file.fieldname + '_' + Date.now() + path.extname(file.originalname));
    }
});

const uploadImage =  multer({
    storage : fileStorage,
    limits : {
        fileSize: 10000000
    },
    fileFilter(request, file, cb) {
        if (!file.originalname.match(/\.(png|jpg|jpeg)$/)) {
            return cb(new Error('Please upload an Image file!'));
        }
        cb(undefined, true);
    }
});

app.post('/uploadImage', uploadImage.array('photo', 20), async (request, response) => {
    const files = request.files;
    console.log(request.body);
    console.log(request.files);
    const imgArray =  await files.map((file) => {
        const img = fs.readFileSync(file.path);
        return img.toString('base64');
    });
    imgArray.map(async (src, index) => {
        const finalImg = {
            title : files[index].originalname,
            path : files[index].path,
            fileSize : files[index].size,
            category : request.body.category
        }
        console.log();
        const newUpload = new Photo(finalImg);

        try {
            await newUpload.save();
            console.log("Upload was good");
            response.json('Upload was successful!!');
        } catch (error) {
            response.status(400).json({error:error.message || `Something went wrong somewhere`});
        }
    });
});

问题根源分析

  1. 异步逻辑错误:使用imgArray.map(async (...))时,map不会等待异步函数执行完成,导致服务器可能在所有数据库保存操作完成前就结束请求,部分文件保存失败无法被捕获。
  2. 重复响应问题:循环中每次调用response.json()或response.status().json(),会尝试多次向客户端发送响应,Node.js会阻止后续响应,导致部分错误无法传递到前端。
  3. 无效的await使用:await files.map(...)无效,因为map返回的是普通数组而非Promise数组,无法等待所有文件读取完成。

修复后的后端代码

// ... 省略其他配置
const fs = require('fs').promises; // 使用fs的promise版本,避免同步阻塞

const fileStorage = multer.diskStorage({
    destination: 'images',
    filename : (request, file, cb) => {
        cb(null, file.fieldname + '_' + Date.now() + path.extname(file.originalname));
    }
});

const uploadImage =  multer({
    storage : fileStorage,
    limits : {
        fileSize: 10000000
    },
    fileFilter(request, file, cb) {
        if (!file.originalname.match(/\.(png|jpg|jpeg)$/)) {
            return cb(new Error('Please upload an Image file!'));
        }
        cb(undefined, true);
    }
});

app.post('/uploadImage', uploadImage.array('photo', 20), async (request, response) => {
    try {
        const files = request.files;
        const category = request.body.category;

        // 用Promise.all等待所有文件处理完成
        const savePromises = files.map(async (file) => {
            // 异步读取文件,避免阻塞事件循环
            // const img = await fs.readFile(file.path);
            // const base64Img = img.toString('base64');
            // 注:如果数据库不需要base64内容,可以省略读取步骤,直接存文件信息
            const finalImg = {
                title: file.originalname,
                path: file.path,
                fileSize: file.size,
                category: category
            };
            const newUpload = new Photo(finalImg);
            return newUpload.save();
        });

        // 等待所有保存操作完成
        await Promise.all(savePromises);
        console.log('所有文件上传成功');
        response.json('所有文件上传成功!!');
    } catch (error) {
        console.error('上传失败:', error);
        response.status(400).json({ error: error.message || '文件保存到数据库失败' });
    }
});

关键修复点说明

  1. 统一异步处理:使用Promise.all等待所有文件的数据库保存操作完成,确保所有文件都处理完毕后再响应客户端。
  2. 避免重复响应:将响应逻辑移到try/catch块的最外层,只发送一次成功或失败响应。
  3. 异步文件读取:使用fs.promises.readFile替代同步的fs.readFileSync,避免阻塞Node.js事件循环,提升并发处理能力。
  4. 前端优化:修复了表单默认提交行为阻止、文件状态初始化、错误提示优化等问题,提升用户体验。

内容的提问来源于stack exchange,提问作者Teke Bethelson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 01:37:33