Skip to content

wangeditor富文本组件+大文件分片上传(秒传,断点续传,分片上穿)

约 4406 字大约 15 分钟

wangeditornode.js

2025-05-18

官网:安装 | wangEditor

1.安装

安装 editor

yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

安装 React 组件(可选)

yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save

安装 Vue2 组件(可选)

yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save

安装 Vue3 组件(可选)

yarn add @wangeditor/editor-for-vue@next
# 或者 npm install @wangeditor/editor-for-vue@next --save

react版本的基本使用

import '@wangeditor/editor/dist/css/style.css' // 引入 css

import React, { useState, useEffect } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'

function MyEditor() {
  // editor 实例
  const [editor, setEditor] = useState<IDomEditor | null>(null) // TS 语法
  // const [editor, setEditor] = useState(null)                   // JS 语法

  // 编辑器内容
  const [html, setHtml] = useState('<p>hello</p>')

  // 模拟 ajax 请求,异步设置 html
  useEffect(() => {
    setTimeout(() => {
      setHtml('<p>hello world</p>')
    }, 1500)
  }, [])

  // 工具栏配置
  const toolbarConfig: Partial<IToolbarConfig> = {} // TS 语法
  // const toolbarConfig = { }                        // JS 语法

  // 编辑器配置
  const editorConfig: Partial<IEditorConfig> = {
    // TS 语法
    // const editorConfig = {                         // JS 语法
    placeholder: '请输入内容...',
  }

  // 及时销毁 editor ,重要!
  useEffect(() => {
    return () => {
      if (editor == null) return
      editor.destroy()
      setEditor(null)
    }
  }, [editor])

  return (
    <>
      <div style={{ border: '1px solid #ccc', zIndex: 100 }}>
        <Toolbar
          editor={editor}
          defaultConfig={toolbarConfig}
          mode="default"
          style={{ borderBottom: '1px solid #ccc' }}
        />
        <Editor
          defaultConfig={editorConfig}
          value={html}
          onCreated={setEditor}
          onChange={(editor) => setHtml(editor.getHtml())}
          mode="default"
          style={{ height: '500px', overflowY: 'hidden' }}
        />
      </div>
      <div style={{ marginTop: '15px' }}>{html}</div>
    </>
  )
}

export default MyEditor

大文件分片上传

一.前端-react+ts

1.使用spark-md5生成文件的hash值

//安装命令
npm i spark-md5

计算文件的 MD5 哈希值

  /**
   * 计算文件的 MD5 哈希值
   * 使用分片读取的方式,避免大文件一次性加载到内存
   * @param file - 要计算哈希的文件对象
   * @returns Promise<string> - 包含文件 MD5 哈希值的 Promise
   */
  const calculateFileHash = (file: File): Promise<string> => {
    // 返回一个 Promise,因为文件读取是异步操作
    return new Promise((resolve, reject) => {
      // 创建 SparkMD5 实例,用于累积计算文件哈希
      const spark = new SparkMD5.ArrayBuffer();
      // 创建 FileReader 实例,用于异步读取文件内容
      const fileReader = new FileReader();
      // 定义分片大小,与上传时使用的 CHUNK_SIZE 保持一致
      const chunkSize = CHUNK_SIZE;
      // 计算文件总共需要分成多少个分片
      const chunks = Math.ceil(file.size / chunkSize);
      // 记录当前正在处理(读取)的分片索引
      let currentChunk = 0;

      // 文件分片读取成功时的回调函数
      fileReader.onload = (e) => {
        // 将读取到的 ArrayBuffer 数据追加到 SparkMD5 实例中
        // e.target?.result 是读取到的文件数据,类型为 ArrayBuffer
        spark.append(e.target?.result as ArrayBuffer);
        // 增加已处理的分片计数
        currentChunk++;

        // 如果还有未读取的分片,则继续读取下一片
        if (currentChunk < chunks) {
          loadNext();
        } else {
          // 如果所有分片都已读取完毕,则计算最终的 MD5 哈希值
          // spark.end() 返回最终的哈希字符串
          resolve(spark.end());
        }
      };

      // 文件读取发生错误时的回调函数
      fileReader.onerror = () => {
        console.error('Error reading file for hash calculation:', fileReader.error);
        // 拒绝 Promise,并传递错误信息
        reject('上传文件错误');
      };

      // 定义一个函数来读取下一个分片
      const loadNext = () => {
        // 计算当前分片的起始字节位置
        const start = currentChunk * chunkSize;
        // 计算当前分片的结束字节位置 (确保不超过文件总大小)
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
        // 使用 file.slice() 获取文件的一个分片 Blob
        // 使用 fileReader.readAsArrayBuffer() 异步读取这个分片的内容为 ArrayBuffer
        fileReader.readAsArrayBuffer(file.slice(start, end));
      };

      // 开始读取第一个分片,触发整个读取和计算流程
      loadNext();
    });
  };

2.文件上传

 /**
   * 将文件分片上传到服务器,支持秒传和断点续传。
   * @param file - 要上传的文件对象。
   * @param fileHash - 文件的 MD5 哈希值。
   * @param insertFn - 编辑器提供的插入函数,用于在文件上传成功后将文件(图片/视频)插入到编辑器中。
   */
  const uploadFileInChunks = async (file: File, fileHash: string, insertFn: (url: string, alt?: string, href?: string) => void) => {
    // 获取文件大小
    const fileSize = file.size;
    // 计算文件总共需要分成多少个分片
    const chunks = Math.ceil(fileSize / CHUNK_SIZE);
    // 初始化一个数组,用于存储已经上传成功的分片索引
    const uploadedChunks: number[] = [];

2.1.检查文件是否已存在或部分上传 (秒传/断点续传检查)

 // 1. 检查文件是否已存在或部分上传 (秒传/断点续传检查)
    try {
      console.log(`检查文件状态: hash=${fileHash}, filename=${file.name}`);
      // 向后端发送 GET 请求,检查文件是否已存在或部分上传
      const checkResponse = await fetch(`http://localhost:9999/upload/check?hash=${fileHash}&filename=${encodeURIComponent(file.name)}`, {
        method: 'GET',
      });
      // 解析后端返回的 JSON 数据
      const checkResult = await checkResponse.json();

      // 检查后端返回的状态码
      if (checkResult.code === 0) {
        // 如果后端指示文件已完全上传 (秒传)
        if (checkResult.data.uploaded) {
          console.log('文件已秒传:', checkResult.data.url);
          // 直接调用编辑器的插入函数,将已存在的文件的 URL 插入到编辑器中
          insertFn(checkResult.data.url, file.name, ''); // 对于图片,alt 和 href 可以根据需要设置
          // 将上传进度设置为 100%
          setUploadProgress(100);
          // 结束上传流程
          return;
        } else {
          // 如果文件未完全上传,但有部分分片已上传 (断点续传)
          console.log('文件部分上传,继续上传。已上传分片:', checkResult.data.uploadedChunks);
          // 将后端返回的已上传分片索引添加到 uploadedChunks 数组中
          uploadedChunks.push(...checkResult.data.uploadedChunks);
        }
      } else {
        // 如果文件检查失败 (后端返回非 0 的 code)
        console.error('文件检查失败:', checkResult.massage);
        // 此时仍然可以尝试从头上传,不做特殊处理
      }
    } catch (error) {
      // 如果文件检查请求本身发生错误 (网络问题等)
      console.error('文件检查请求出错:', error);
      // 此时也可以尝试从头上传,不做特殊处理
    }

2.2上传未完成的分片

  // 2. 上传未完成的分片
    // 创建一个数组,用于存储每个分片上传的 Promise 对象
    const uploadPromises: Promise<void>[] = [];
    // 初始化已完成的分片数量,考虑断点续传已上传的分片
    let uploadedCount = uploadedChunks.length;
    // 计算需要上传的分片数量
    const totalChunksToUpload = chunks - uploadedChunks.length;

    // 定义一个函数来更新总上传进度
    const updateOverallProgress = () => {
      // 计算总上传进度百分比
      const progress = Math.floor((uploadedCount / chunks) * 100);
      // 更新状态中的上传进度
      setUploadProgress(progress);
      console.log(`总上传进度: ${progress}%,未上传分片:${totalChunksToUpload}`);
    };

    // 初始进度,考虑到已上传的分片
    updateOverallProgress();

    // 遍历所有分片
    for (let i = 0; i < chunks; i++) {
      // 如果当前分片已经在已上传列表中,则跳过此次循环,不上传该分片
      if (uploadedChunks.includes(i)) {
        continue;
      }

      // 计算当前分片的起始和结束字节位置
      const start = i * CHUNK_SIZE;
      const end = ((start + CHUNK_SIZE) >= fileSize) ? fileSize : start + CHUNK_SIZE;
      // 从文件中切出当前分片的数据 (Blob 对象)
      const chunk = file.slice(start, end);

      // 创建 FormData 对象,用于构建分片上传的请求体
      const formData = new FormData();
      formData.append('fileHash', fileHash); // 添加文件哈希
      formData.append('chunkIndex', i.toString()); // 添加当前分片索引
      formData.append('totalChunks', chunks.toString()); // 添加总分片数
      formData.append('filename', file.name); // 添加文件名
      formData.append('fileSize', fileSize.toString()); // 添加文件总大小
      formData.append('chunk', chunk); // 添加分片数据本身 (Blob)

      // 使用 Promise 包裹 XMLHttpRequest 请求,以便使用 Promise.all 管理并行上传
      const uploadPromise = new Promise<void>((resolve, reject) => {
        // 创建 XMLHttpRequest 实例
        const xhr = new XMLHttpRequest();
        // 配置 POST 请求,指定上传分片的 URL
        xhr.open('POST', 'http://localhost:9999/upload/chunk', true);

        // 监听上传进度事件(可选,用于显示单个分片进度,这里主要依赖总进度)
        xhr.upload.onprogress = (event) => {
          if (event.lengthComputable) {
            const percentComplete = (event.loaded / event.total) * 100;
            // console.log(`分片 ${i} 进度: ${percentComplete}%`); // 可以打印单个分片进度
          }
        };

        // 监听请求完成事件 (无论成功或失败)
        xhr.onload = () => {
          // 检查 HTTP 状态码是否为 200 (表示请求成功到达服务器并处理)
          if (xhr.status === 200) {
            try {
              // 解析后端返回的 JSON 数据
              const result = JSON.parse(xhr.responseText);
              // 检查后端返回的业务逻辑状态码
              if (result.code === 0) {
                console.log(`分片 ${i} 上传成功`);
                // 增加已成功上传的分片计数
                uploadedCount++;
                // 更新总上传进度
                updateOverallProgress();
                // 标记此 Promise 成功完成
                resolve();
              } else {
                // 如果后端返回非 0 的 code,表示业务逻辑错误
                console.error(`分片 ${i} 上传失败:`, result.massage);
                // 可以实现重试逻辑
                // 标记此 Promise 失败,并传递错误信息
                reject(`分片 ${i} 上传失败: ${result.massage}`);
              }
            } catch (e) {
               // 解析 JSON 失败
               console.error(`分片 ${i} 响应解析失败:`, e);
               reject(`分片 ${i} 响应解析失败`);
            }
          } else {
            // 如果 HTTP 状态码不是 200,表示请求失败
            console.error(`分片 ${i} 上传请求失败: HTTP状态码 ${xhr.status}`);
            // 可以实现重试逻辑
            // 标记此 Promise 失败
            reject(`分片 ${i} 上传请求失败: HTTP状态码 ${xhr.status}`);
          }
        };

        // 监听网络错误事件
        xhr.onerror = (err) => {
          console.error(`分片 ${i} 上传出错:`, err);
          // 可以实现重试逻辑
          // 标记此 Promise 失败
          reject(`分片 ${i} 上传出错`);
        };

        // 监听上传超时事件
        xhr.ontimeout = () => {
          console.error(`分片 ${i} 上传超时`);
          // 标记此 Promise 失败
          reject(`分片 ${i} 上传超时`);
        };

        // 发送 FormData 请求
        xhr.send(formData);
      });

      // 将当前分片上传的 Promise 添加到数组中
      uploadPromises.push(uploadPromise);
    }

2.3等待所有分片上传完成

	// 3. 等待所有分片上传完成
    try {
      // 使用 Promise.all 等待 uploadPromises 数组中的所有 Promise 都成功完成
      await Promise.all(uploadPromises);
      console.log('所有分片上传完成,通知后端合并。');

2.4通知后端合并分片

 // 4. 通知后端合并分片
      // 向后端发送 POST 请求,通知合并分片
      const mergeResponse = await fetch('http://localhost:9999/upload/merge', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json', // 指定请求体类型为 JSON
        },
        // 将合并所需的数据转换为 JSON 字符串作为请求体
        body: JSON.stringify({
          fileHash: fileHash, // 文件哈希
          filename: file.name, // 文件名
          totalChunks: chunks, // 总分片数
        }),
      });

      // 解析后端返回的 JSON 数据
      const mergeResult = await mergeResponse.json();

      // 检查后端返回的业务逻辑状态码和数据
      if (mergeResult.code === 0 && mergeResult.data) {
        console.log('文件合并成功,最终 URL:', mergeResult.data);
        // 调用编辑器的插入函数,将合并后的文件的最终 URL 插入到编辑器中
        insertFn(mergeResult.data, file.name, ''); // 对于图片,alt 和 href 可以根据需要设置
        // 将上传进度设置为 100%
        setUploadProgress(100);
      } else {
        // 如果合并失败
        console.error('文件合并失败:', mergeResult.massage);
        // 弹出提示框告知用户合并失败
        alert(`文件合并失败: ${mergeResult.massage || '未知错误'}`);
        // 将进度重置为 0
        setUploadProgress(0);
      }

    } catch (error) {
      // 如果在分片上传或合并过程中发生任何错误 (Promise.all 中的某个 Promise 失败,或合并请求失败)
      console.error('分片上传或合并过程中发生错误:', error);
      // 弹出提示框告知用户上传或合并过程中发生错误
      alert('文件上传或合并过程中发生错误,请重试。');
      // 将进度重置为 0
      setUploadProgress(0);
    }
  };

二.后端-node.js+express+Multer

1.使用spark-md5生成文件的hash值

//安装命令
npm i spark-md5

2.大文件分片上传相关接口

2.1定义文件存储路径


// 定义文件存储路径
const UPLOAD_DIR = path.join(__dirname, '../public/uploads'); // 最终文件存储目录
const TEMP_CHUNK_DIR = path.join(__dirname, '../public/temp/chunks'); // 临时分片存储目录
const FILE_MAP_PATH = path.join(__dirname, '../fileMap.json'); // 文件哈希与最终路径的映射文件 

// 确保上传目录和临时目录存在
if (!fs.existsSync(UPLOAD_DIR)) {
    fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
if (!fs.existsSync(TEMP_CHUNK_DIR)) {
    fs.mkdirSync(TEMP_CHUNK_DIR, { recursive: true });
}

2.2 加载文件哈希映射和保存

let fileMap = {};
if (fs.existsSync(FILE_MAP_PATH)) {
    try {
        fileMap = JSON.parse(fs.readFileSync(FILE_MAP_PATH, 'utf-8'));
        console.log('Loaded file map:', fileMap);
    } catch (e) {
        console.error('Error loading file map:', e);
        fileMap = {}; // 加载失败则初始化为空对象
    }
}

// 保存文件哈希映射
const saveFileMap = () => {
    fs.writeFileSync(FILE_MAP_PATH, JSON.stringify(fileMap, null, 2));
};

2.3Multer 配置用于接收单个分片


const chunkStorage = multer.diskStorage({
    destination: function (req, file, cb) {
        // 分片存储在临时目录下的以文件哈希命名的子目录中
        const fileHash = req.body.fileHash;
        if (!fileHash) {
            return cb(new Error('未找到文件哈希'));
        }
        const chunkDir = path.join(TEMP_CHUNK_DIR, fileHash);
        // 确保分片目录存在
        if (!fs.existsSync(chunkDir)) {
            fs.mkdirSync(chunkDir, { recursive: true });
        }
        cb(null, chunkDir);
    },
    filename: function (req, file, cb) {
        // 分片文件名就是其序号
        const chunkIndex = req.body.chunkIndex;
         if (chunkIndex === undefined) {
            return cb(new Error(' 未找到分片序号'));
        }
        cb(null, chunkIndex.toString());
    }
});

const uploadChunk = multer({
    storage: chunkStorage,
    limits: {
         fileSize: 10 * 1024 * 1024 // 10MB
    },
    fileFilter: function (req, file, cb) {
        // 可以根据需要添加文件类型校验,但通常在接收分片时不强制校验类型
        // 最终文件的类型校验可以在合并时进行,或者依赖前端的校验
        cb(null, true); // 允许所有文件类型作为分片
    }
}).single('chunk'); // 接收名为 'chunk' 的文件字段

2.4检查文件是否存在或已上传的分片

router.get('/upload/check', (req, res) => {
    const fileHash = req.query.hash;
    const filename = req.query.filename; // 前端传递的文件名,用于查找映射
    console.log(`Checking file: hash=${fileHash}, filename=${filename}`);

    if (!fileHash) {
        return res.status(400).json({ code: 1, massage: 'Missing file hash' });
    }

    // 1. 检查最终文件是否已存在 (秒传)
    // 从映射中查找文件哈希对应的最终文件路径
    if (fileMap[fileHash]) {
         const finalFilePath = path.join(UPLOAD_DIR, fileMap[fileHash].filename); // 从映射中获取保存的文件名
         // 双重确认文件确实存在
         if (fs.existsSync(finalFilePath)) {
             console.log(`File exists (instant upload): ${finalFilePath}`);
             // 返回最终文件的可访问 URL
             const fileUrl = `/uploads/${fileMap[fileHash].filename}`; // 根据你的静态服务配置调整 URL 格式
             return res.json({
                 code: 0,
                 massage: '文件已存在,秒传成功',
                 data: {
                     uploaded: true,
                     url: `http://127.0.0.1:9999${fileUrl}` // 返回完整 URL
                 }
             });
         } else {
             console.warn(`File map entry found for ${fileHash}, but file ${finalFilePath} does not exist. Removing map entry.`);
             delete fileMap[fileHash]; // 清理无效的映射
             saveFileMap();
         }
    }


    // 2. 检查临时目录中已上传的分片 (断点续传)
    const chunkDir = path.join(TEMP_CHUNK_DIR, fileHash);
    const uploadedChunks= [];

    if (fs.existsSync(chunkDir)) {
        const files = fs.readdirSync(chunkDir);
        files.forEach(file => {
            const chunkIndex = parseInt(file, 10);
            // 确保是有效的数字文件名
            if (!isNaN(chunkIndex)) {
                uploadedChunks.push(chunkIndex);
            }
        });
        console.log(`找到文件chunks for ${fileHash}: ${uploadedChunks}`);
    } else {
        console.log(`没有找到hash ${fileHash}`);
    }

    // 返回已上传的分片列表
    res.json({
        code: 0,
        massage: '文件未完全上传',
        data: {
            uploaded: false,
            uploadedChunks: uploadedChunks
        }
    });
});

2.5接收文件分片

router.post('/upload/chunk', (req, res) => {
    uploadChunk(req, res, function (err) {
        if (err instanceof multer.MulterError) {
            console.error('Multer error uploading chunk:', err);
            return res.status(400).json({ code: 1, massage: `分片上传失败: ${err.message}` });
        } else if (err) {
            console.error('Unknown error uploading chunk:', err);
             return res.status(500).json({ code: 1, massage: `分片上传失败: ${err.message}` });
        }

        // Multer 成功处理后,文件信息在 req.file
        if (!req.file) {
             console.error('Chunk file not received by multer.');
            return res.status(400).json({ code: 1, massage: '未接收到文件分片' });
        }

        // req.body 中包含其他字段
        const { fileHash, chunkIndex, totalChunks, filename, fileSize } = req.body;

        if (!fileHash || chunkIndex === undefined || totalChunks === undefined || !filename || fileSize === undefined) {
             // 清理已接收到的分片文件,因为缺少必要参数
             fs.unlink(req.file.path, (unlinkErr) => {
                 if (unlinkErr) console.error('Error deleting incomplete chunk file:', unlinkErr);
             });
            console.error('Missing required fields in chunk upload request body.');
            return res.status(400).json({ code: 1, massage: '缺少必要的分片信息' });
        }

        console.log(`Received chunk ${chunkIndex}/${totalChunks} for file ${fileHash}`);

        // 分片已成功保存到临时目录,返回成功响应
        res.json({ code: 0, massage: '分片上传成功' });
    });
});

2.6合并文件分片

router.post('/upload/merge', async (req, res) => {
    const { fileHash, filename, totalChunks } = req.body;
    console.log(`Merging file: hash=${fileHash}, filename=${filename}, totalChunks=${totalChunks}`);

    if (!fileHash || !filename || totalChunks === undefined) {
        return res.status(400).json({ code: 1, massage: '缺少必要的文件合并信息' });
    }

    const chunkDir = path.join(TEMP_CHUNK_DIR, fileHash);
    const finalFileName = `${fileHash}_${filename}`; // 使用哈希作为前缀避免文件名冲突
    const finalFilePath = path.join(UPLOAD_DIR, finalFileName);

    // 检查所有分片是否存在
    const existingChunks = fs.existsSync(chunkDir) ? fs.readdirSync(chunkDir).length : 0;
    if (existingChunks < totalChunks) {
        console.error(`Missing chunks for merge. Expected ${totalChunks}, found ${existingChunks}.`);
        // 可以选择清理已上传的分片,或者保留等待后续上传
        // 在这个示例中,我们直接返回错误
        return res.status(400).json({ code: 1, massage: `分片数量不完整,无法合并。已找到 ${existingChunks} 个分片,需要 ${totalChunks} 个。` });
    }

    // 检查分片是否按序号命名且数量正确
     const chunkFiles = fs.readdirSync(chunkDir).map(f => parseInt(f, 10)).sort((a, b) => a - b);
     if (chunkFiles.length !== totalChunks || chunkFiles[0] !== 0 || chunkFiles[chunkFiles.length - 1] !== totalChunks - 1) {
         console.error('Chunk files are not sequentially numbered or count is incorrect.');
         return res.status(400).json({ code: 1, massage: '分片文件序号或数量不正确,无法合并。' });
     }


    // 合并分片
    try {
        const writeStream = fs.createWriteStream(finalFilePath);

        for (let i = 0; i < totalChunks; i++) {
            const chunkFilePath = path.join(chunkDir, i.toString());
            const readStream = fs.createReadStream(chunkFilePath);
            // 使用管道流将分片内容写入最终文件
            // 等待当前分片写入完成后再处理下一个分片,避免文件损坏
            await new Promise((resolve, reject) => {
                readStream.pipe(writeStream, { end: false }); // end: false 阻止 writeStream 在 readStream 结束时关闭
                readStream.on('end', resolve);
                readStream.on('error', reject);
            });
             // 可选:合并完一个分片后删除临时分片文件
             fs.unlink(chunkFilePath, (err) => {
                 if (err) console.error(`Error deleting chunk file ${chunkFilePath}:`, err);
             });
        }
        // 所有分片写入完成后,关闭主写入流
        writeStream.end();

        // 等待 writeStream 真正关闭
        await new Promise((resolve, reject) => {
             writeStream.on('finish', resolve);
             writeStream.on('error', reject);
        });
        console.log(`File merged successfully: ${finalFilePath}`);
        // 清理临时分片目录
        fs.rmdir(chunkDir, { recursive: true }, (err) => {
            if (err) console.error(`Error deleting chunk directory ${chunkDir}:`, err);
            else console.log(`Chunk directory cleaned: ${chunkDir}`);
        });

        // 更新文件哈希映射
        fileMap[fileHash] = {
            filename: finalFileName, // 保存服务器上实际存储的文件名
            originalFilename: filename,
            uploadTime: new Date().toISOString()
        };
        saveFileMap();
        // 返回最终文件的可访问 URL
        const fileUrl = `/uploads/${finalFileName}`; // 根据你的静态服务配置调整 URL 格式
        res.json({
            code: 0,
            massage: '文件合并成功',
            data: `http://127.0.0.1:9999${fileUrl}` // 返回完整 URL
        });

    } catch (error) {
        console.error('Error merging file:', error);
        // 合并失败,可能需要清理已写入的部分最终文件和临时分片
        // 在这个示例中,我们只返回错误
        res.status(500).json({ code: 1, massage: `文件合并失败: ${error.message}` });
    }
});

2.7中间件错误抛出 (保留并增强对 Multer 错误的捕获)

router.use((err, req, res, next) => {
    if (err instanceof multer.MulterError) {
        console.error('Multer Error:', err.message, err.code);
        let message = '文件上传错误';
        if (err.code === 'LIMIT_FILE_SIZE') {
             // 尝试获取限制大小,这里需要知道是哪个 Multer 实例抛出的错误
             // 对于 chunkUpload,限制是 10MB
             const limitMB = 10; // 硬编码或从 multer 配置中获取
             message = `文件大小超出限制 (${limitMB}MB)`;
        } else {
            message = `文件上传错误: ${err.message}`;
        }
        res.status(400).send({
            code: 1,
            massage: message
        });
    } else if (err) {
        console.error('File Upload Error:', err.message);
        res.status(400).send({
            code: 1,
            massage: err.message || '文件上传失败'
        });
    } else {
        next();
    }
});