大文件的切片上传和断点续传前后端(Vue+node.js)具体实现

发布于:2025-07-30 ⋅ 阅读:(31) ⋅ 点赞:(0)

技术栈与插件说明

前端技术栈

  • Vue3 + Vite:构建用户界面和前端工程化
  • SparkMD5:计算文件哈希值,用于唯一标识文件

后端技术栈

  • Node.js + Express:搭建 HTTP 服务器和接口
  • multiparty:处理表单数据,解析上传的分片文件
  • fs-extra:文件系统操作的增强库,支持递归创建目录、移动文件等
  • cors:处理跨域请求,允许前端访问后端接口
  • body-parser:解析 HTTP 请求体,支持 JSON 和表单数据
  • nodemon(开发依赖):监测文件变化并自动重启服务器,提高开发效率
插件 作用 前端 / 后端
SparkMD5 计算文件哈希值,用于唯一标识文件(断点续传核心) 前端
Express 快速搭建 HTTP 服务器,提供路由和中间件支持 后端
multiparty 解析包含文件的表单数据,提取上传的分片文件 后端
fs-extra 扩展 Node.js 原生 fs 模块,支持递归创建目录、安全移动文件等 后端
cors 配置跨域规则,允许前端域名访问后端接口 后端
body-parser 解析 JSON 和表单格式的请求体,方便获取接口参数 后端
nodemon 开发时自动重启服务器,无需手动重启即可生效代码变更 后端(开发依赖)

项目搭建步骤

前端项目搭建

# 创建Vue3项目(使用Vite模板)
npm create vite@latest file-upload-frontend -- --template vue
cd file-upload-frontend

# 安装依赖
npm install
# 安装文件哈希计算库
npm install spark-md5

后端项目搭建

# 创建后端目录并初始化
mkdir file-upload-backend && cd file-upload-backend
npm init -y

# 安装核心依赖
npm install express multiparty fs-extra cors body-parser
# 安装开发依赖(热重载)
npm install -D nodemon

前端实现:核心逻辑拆解

前端的核心任务是:将文件切片、计算哈希、并发上传分片、请求合并分片。我们按逻辑拆分为以下函数。

// 文件切片函数:将文件按指定大小分割为多个Blob
const createChunks = (file) => {
  const chunkSize = 1024 * 1024; // 1MB/片(可根据需求调整)
  let cur = 0; // 当前切片位置
  const chunks = [];
  
  while (cur < file.size) {
    // 从cur位置开始,截取chunkSize大小的内容作为一个分片
    const blob = file.slice(cur, cur + chunkSize);
    chunks.push(blob);
    cur += chunkSize;
  }
  
  return chunks; // 返回所有分片数组
};

作用:避免一次性加载大文件到内存,降低浏览器内存占用,同时实现分片上传。

2. 哈希计算:生成文件唯一标识

// 计算文件哈希值:用于标识文件唯一性(断点续传核心)
const calculateHash = (chunks) => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer(); // 创建SparkMD5实例
    const fileReader = new FileReader(); // 文件读取器
    
    // 优化:大文件无需完整计算,取关键部分即可
    const targets = [];
    chunks.forEach((chunk, index) => {
      if (index === 0 || index === chunks.length - 1) {
        // 首尾分片完整计算
        targets.push(chunk);
      } else {
        // 中间分片取部分内容(前2字节+中间2字节+后2字节)
        targets.push(chunk.slice(0, 2));
        targets.push(chunk.slice(chunk.size / 2, chunk.size / 2 + 2));
        targets.push(chunk.slice(-2));
      }
    });
    
    // 读取选中的内容并计算哈希
    fileReader.readAsArrayBuffer(new Blob(targets));
    fileReader.onload = (e) => {
      spark.append(e.target.result); // 添加内容到哈希计算
      const hash = spark.end(); // 完成计算,获取哈希值
      resolve(hash);
    };
  });
};

作用:通过哈希值唯一标识文件,后端可通过哈希判断文件是否已上传、哪些分片已上传,实现断点续传。

3. 断点续传检查:获取已上传分片

// 检查文件上传状态:获取已上传的分片,避免重复上传
const checkFileUploadStatus = async (hash, fileName) => {
  try {
    const res = await fetch('http://localhost:3000/check-file', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ fileHash: hash, fileName })
    });
    return res.json();
  } catch (err) {
    console.error('检查文件状态失败:', err);
    return { exists: false, uploadedChunks: [] };
  }
};

作用:上传前先询问后端,该文件是否已完整上传?如果没有,哪些分片已经上传?以此实现「断点续传」—— 只传未完成的分片。

4. 并发上传分片:控制上传速度

// 并发上传分片:控制同时上传的分片数量
const uploadChunksConcurrently = async (chunksToUpload, maxConcurrent = 6) => {
  const taskPool = []; // 存储当前正在执行的上传任务
  let completed = 0; // 已完成的分片数量
  const total = chunksToUpload.length; // 需上传的总分片数
  
  for (let i = 0; i < chunksToUpload.length; i++) {
    // 创建单个分片的上传任务
    const task = fetch('http://localhost:3000/upload-chunk', {
      method: 'POST',
      body: chunksToUpload[i].formData
    })
    .then(res => res.json())
    .then(() => {
      completed++;
      // 计算并更新进度(可根据需求渲染到UI)
      const progress = Math.floor((completed / total) * 100);
      console.log(`上传进度: ${progress}%`);
    })
    .catch(err => {
      console.error(`分片 ${i} 上传失败:`, err);
      throw err; // 抛出错误,便于后续捕获
    });
    
    taskPool.push(task);
    
    // 当并发数达到上限,等待任一任务完成后再继续
    if (taskPool.length >= maxConcurrent) {
      await Promise.race(taskPool);
      // 移除已完成的任务(保持任务池大小不超过上限)
      taskPool.splice(taskPool.findIndex(t => t.isResolved), 1);
    }
  }
  
  // 等待剩余任务全部完成
  await Promise.all(taskPool);
  console.log('所有分片上传完成');
};

作用:控制同时上传的分片数量(默认 6 个),避免一次性发起过多请求导致浏览器或服务器压力过大。

5. 合并分片:组装完整文件

// 通知后端合并分片
const mergeChunks = async (hash, fileName, totalChunks) => {
  try {
    const res = await fetch('http://localhost:3000/merge-chunks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileHash: hash,
        fileName,
        totalChunks
      })
    });
    return res.json();
  } catch (err) {
    console.error('请求合并分片失败:', err);
    return { success: false, error: '合并请求失败' };
  }
};

作用:所有分片上传完成后,通知后端将分片按顺序合并为完整文件。

6. 主流程函数:串联所有步骤

// 主上传流程:串联文件处理、检查状态、上传分片、合并文件
const handleFileUpload = async (file) => {
  if (!file) return;
  
  // 1. 切片:将文件分割为分片
  const chunks = createChunks(file);
  console.log(`文件已分割为 ${chunks.length} 个分片`);
  
  // 2. 计算哈希:生成文件唯一标识
  const hash = await calculateHash(chunks);
  console.log('文件哈希值:', hash);
  
  // 3. 检查状态:获取已上传的分片
  const checkResult = await checkFileUploadStatus(hash, file.name);
  if (checkResult.exists) {
    console.log('文件已完整上传,无需重复上传');
    return;
  }
  
  // 4. 准备需上传的分片(过滤已上传的分片)
  const chunksToUpload = chunks
    .map((chunk, index) => ({
      index,
      chunkHash: `${hash}-${index}`,
      formData: createFormData(hash, `${hash}-${index}`, chunk)
    }))
    .filter(item => !checkResult.uploadedChunks.includes(item.chunkHash));
  
  if (chunksToUpload.length === 0) {
    console.log('所有分片已上传,准备合并');
  } else {
    // 5. 并发上传未完成的分片
    await uploadChunksConcurrently(chunksToUpload);
  }
  
  // 6. 合并分片:所有分片上传完成后,请求合并
  const mergeResult = await mergeChunks(hash, file.name, chunks.length);
  if (mergeResult.success) {
    console.log('文件上传成功!保存路径:', mergeResult.filePath);
  } else {
    console.error('文件合并失败:', mergeResult.error);
  }
};

// 辅助函数:创建分片上传的FormData
const createFormData = (fileHash, chunkHash, chunk) => {
  const formData = new FormData();
  formData.append('chunk', chunk); // 分片文件
  formData.append('fileHash', fileHash); // 文件哈希
  formData.append('chunkHash', chunkHash); // 分片哈希(唯一标识分片)
  return formData;
};

作用:串联「切片→哈希→检查→上传→合并」的完整流程,实现大文件的分片上传。

前端完整组件代码

<template>
  <div class="upload-container">
    <input type="file" @change="handleFileSelect" class="file-input" />
    <button @click="startUpload" class="upload-btn">上传文件</button>
    <div class="progress" v-if="progress > 0">
      <div class="progress-bar" :style="{ width: progress + '%' }"></div>
      <span class="progress-text">{{ progress }}%</span>
    </div>
    <p class="status">{{ statusText }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import SparkMD5 from 'spark-md5';

// 状态变量
const file = ref(null);
const progress = ref(0);
const statusText = ref('');
const chunkSize = 1024 * 1024; // 1MB/分片

// 1. 文件切片
const createChunks = (file) => {
  let cur = 0;
  const chunks = [];
  while (cur < file.size) {
    chunks.push(file.slice(cur, cur + chunkSize));
    cur += chunkSize;
  }
  return chunks;
};

// 2. 计算文件哈希
const calculateHash = (chunks) => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    const targets = [];

    chunks.forEach((chunk, index) => {
      if (index === 0 || index === chunks.length - 1) {
        targets.push(chunk);
      } else {
        targets.push(chunk.slice(0, 2));
        targets.push(chunk.slice(chunk.size / 2, chunk.size / 2 + 2));
        targets.push(chunk.slice(-2));
      }
    });

    fileReader.readAsArrayBuffer(new Blob(targets));
    fileReader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
};

// 3. 检查文件上传状态
const checkFileUploadStatus = async (hash, fileName) => {
  statusText.value = '检查文件状态...';
  try {
    const res = await fetch('http://localhost:3000/check-file', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ fileHash: hash, fileName })
    });
    return res.json();
  } catch (err) {
    statusText.value = '检查文件状态失败';
    return { exists: false, uploadedChunks: [] };
  }
};

// 4. 创建FormData
const createFormData = (fileHash, chunkHash, chunk) => {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('fileHash', fileHash);
  formData.append('chunkHash', chunkHash);
  return formData;
};

// 5. 并发上传分片
const uploadChunksConcurrently = async (chunksToUpload) => {
  const maxConcurrent = 6;
  const taskPool = [];
  let completed = 0;
  const total = chunksToUpload.length;

  for (let i = 0; i < chunksToUpload.length; i++) {
    const { formData, index } = chunksToUpload[i];
    const task = fetch('http://localhost:3000/upload-chunk', {
      method: 'POST',
      body: formData
    })
    .then(res => res.json())
    .then(() => {
      completed++;
      progress.value = Math.floor((completed / total) * 100);
      statusText.value = `上传中: ${progress.value}%`;
    })
    .catch(err => {
      statusText.value = `分片 ${index} 上传失败`;
      throw err;
    });

    taskPool.push(task);
    if (taskPool.length >= maxConcurrent) {
      await Promise.race(taskPool);
      taskPool.splice(taskPool.findIndex(t => t.isResolved), 1);
    }
  }

  await Promise.all(taskPool);
};

// 6. 合并分片
const mergeChunks = async (hash, fileName, totalChunks) => {
  statusText.value = '正在合并文件...';
  try {
    const res = await fetch('http://localhost:3000/merge-chunks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ fileHash: hash, fileName, totalChunks })
    });
    return res.json();
  } catch (err) {
    statusText.value = '合并文件失败';
    return { success: false };
  }
};

// 7. 主上传流程
const startUpload = async () => {
  if (!file.value) {
    statusText.value = '请先选择文件';
    return;
  }
  progress.value = 0;
  statusText.value = '准备上传...';

  try {
    const chunks = createChunks(file.value);
    const hash = await calculateHash(chunks);
    const checkResult = await checkFileUploadStatus(hash, file.value.name);

    if (checkResult.exists) {
      statusText.value = '文件已存在,无需上传';
      return;
    }

    const chunksToUpload = chunks
      .map((chunk, index) => ({
        index,
        chunkHash: `${hash}-${index}`,
        formData: createFormData(hash, `${hash}-${index}`, chunk)
      }))
      .filter(item => !checkResult.uploadedChunks.includes(item.chunkHash));

    if (chunksToUpload.length > 0) {
      await uploadChunksConcurrently(chunksToUpload);
    }

    const mergeResult = await mergeChunks(hash, file.value.name, chunks.length);
    if (mergeResult.success) {
      progress.value = 100;
      statusText.value = '文件上传成功!';
    } else {
      statusText.value = '合并失败: ' + mergeResult.error;
    }
  } catch (err) {
    statusText.value = '上传失败: ' + err.message;
  }
};

// 处理文件选择
const handleFileSelect = (e) => {
  file.value = e.target.files[0];
  if (file.value) {
    statusText.value = `已选择文件: ${file.value.name}`;
  }
};

// 扩展Promise,添加状态跟踪(用于并发控制)
Promise.prototype.isResolved = false;
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
  const result = originalThen.apply(this, args);
  this.isResolved = true;
  return result;
};
</script>

<style scoped>
.upload-container {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  border: 1px solid #eee;
  border-radius: 8px;
}

.file-input {
  display: block;
  margin-bottom: 1rem;
  padding: 0.5rem;
  width: 100%;
}

.upload-btn {
  background: #42b983;
  color: white;
  border: none;
  padding: 0.7rem 1.5rem;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 1rem;
}

.upload-btn:hover {
  background: #359e75;
}

.progress {
  height: 20px;
  border-radius: 10px;
  background: #f0f0f0;
  overflow: hidden;
  position: relative;
  margin: 1rem 0;
}

.progress-bar {
  height: 100%;
  background: #42b983;
  transition: width 0.3s;
}

.progress-text {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.8rem;
  color: #333;
}

.status {
  color: #666;
  min-height: 1.5rem;
}
</style>

后端实现:核心逻辑拆解

1. 服务器初始化与配置

const express = require('express');
const multiparty = require('multiparty');
const fs = require('fs-extra');
const path = require('path');
const cors = require('cors');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

// 配置存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'uploads'); // 最终文件存储目录
const TEMP_DIR = path.resolve(__dirname, 'temp'); // 分片临时存储目录

// 确保目录存在(不存在则创建)
fs.ensureDirSync(UPLOAD_DIR);
fs.ensureDirSync(TEMP_DIR);

// 配置中间件
app.use(cors({ origin: 'http://localhost:5173' })); // 允许前端跨域访问
app.use(bodyParser.json()); // 解析JSON请求体
app.use(bodyParser.urlencoded({ extended: true })); // 解析表单请求体

作用:初始化服务器,配置跨域、请求解析,创建文件存储目录。

2. /check-file 接口:检查文件上传状态

// 检查文件是否已上传、已上传哪些分片
app.post('/check-file', async (req, res) => {
  try {
    const { fileHash, fileName } = req.body;
    const filePath = path.join(UPLOAD_DIR, fileName); // 完整文件路径
    
    // 检查文件是否已完整上传
    if (await fs.pathExists(filePath)) {
      return res.json({ exists: true, uploadedChunks: [] });
    }
    
    // 检查已上传的分片:分片存储在以fileHash命名的临时目录
    const chunkDir = path.join(TEMP_DIR, fileHash);
    let uploadedChunks = [];
    
    if (await fs.pathExists(chunkDir)) {
      // 读取目录下的所有分片文件,提取分片标识
      const chunks = await fs.readdir(chunkDir);
      uploadedChunks = chunks.map(chunk => chunk.replace('.chunk', ''));
    }
    
    res.json({ exists: false, uploadedChunks });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

作用:响应前端的断点续传检查请求,返回文件是否完整上传、已上传的分片列表。

3. /upload-chunk 接口:接收分片文件

// 接收并存储分片文件
app.post('/upload-chunk', (req, res) => {
  // 使用multiparty解析包含文件的表单数据
  const form = new multiparty.Form({ uploadDir: TEMP_DIR });
  
  form.parse(req, async (err, fields, files) => {
    if (err) {
      return res.status(500).json({ error: err.message });
    }
    
    try {
      // 从表单字段中获取哈希信息
      const fileHash = fields.fileHash[0];
      const chunkHash = fields.chunkHash[0];
      
      // 获取上传的分片文件临时路径
      const chunkFile = files.chunk[0];
      
      // 创建分片存储目录(以fileHash命名,避免不同文件分片冲突)
      const chunkDir = path.join(TEMP_DIR, fileHash);
      await fs.ensureDir(chunkDir);
      
      // 将临时分片移动到目标目录,并重命名
      const destPath = path.join(chunkDir, `${chunkHash}.chunk`);
      await fs.move(chunkFile.path, destPath, { overwrite: true });
      
      res.json({ success: true, message: `分片 ${chunkHash} 上传成功` });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });
});

作用:接收前端上传的分片文件,存储到以文件哈希命名的临时目录,确保分片不冲突

4. /merge-chunks 接口:合并分片为完整文件

// 合并分片为完整文件
app.post('/merge-chunks', async (req, res) => {
  try {
    const { fileHash, fileName, totalChunks } = req.body;
    const chunkDir = path.join(TEMP_DIR, fileHash); // 分片存储目录
    const destPath = path.join(UPLOAD_DIR, fileName); // 合并后的文件路径
    
    // 检查分片目录是否存在
    if (!await fs.pathExists(chunkDir)) {
      return res.status(400).json({ error: '分片不存在' });
    }
    
    // 获取所有分片并检查数量是否完整
    const chunks = await fs.readdir(chunkDir);
    if (chunks.length !== totalChunks) {
      return res.status(400).json({ 
        error: '分片不完整', 
        received: chunks.length, 
        total: totalChunks 
      });
    }
    
    // 按分片索引排序(确保合并顺序正确)
    chunks.sort((a, b) => {
      const indexA = parseInt(a.split('-')[1].replace('.chunk', ''));
      const indexB = parseInt(b.split('-')[1].replace('.chunk', ''));
      return indexA - indexB;
    });
    
    // 创建可写流,合并分片
    const writeStream = fs.createWriteStream(destPath);
    for (const chunk of chunks) {
      const chunkPath = path.join(chunkDir, chunk);
      const chunkBuffer = await fs.readFile(chunkPath); // 读取分片内容
      // 写入到目标文件
      await new Promise((resolve, reject) => {
        writeStream.write(chunkBuffer, (err) => {
          if (err) reject(err);
          else resolve();
        });
      });
    }
    
    // 完成合并,关闭流并删除临时分片目录
    writeStream.end();
    await fs.remove(chunkDir); // 清理临时文件
    
    res.json({ 
      success: true, 
      message: '文件合并成功',
      filePath: destPath 
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

作用:接收合并请求,检查分片完整性,按顺序合并分片为完整文件,最后清理临时分片。

后端完整代码

const express = require('express');
const multiparty = require('multiparty');
const fs = require('fs-extra');
const path = require('path');
const cors = require('cors');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

// 配置存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'uploads'); // 最终文件存储目录
const TEMP_DIR = path.resolve(__dirname, 'temp'); // 分片临时存储目录

// 确保目录存在
fs.ensureDirSync(UPLOAD_DIR);
fs.ensureDirSync(TEMP_DIR);

// 配置中间件
app.use(cors({ origin: 'http://localhost:5173' })); // 允许前端跨域
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

/**
 * 1. 检查文件上传状态接口
 * 作用:判断文件是否已完整上传,返回已上传的分片列表
 */
app.post('/check-file', async (req, res) => {
  try {
    const { fileHash, fileName } = req.body;
    const filePath = path.join(UPLOAD_DIR, fileName);
    
    // 检查文件是否已完整上传
    if (await fs.pathExists(filePath)) {
      return res.json({ 
        exists: true, 
        uploadedChunks: [],
        message: '文件已完整上传'
      });
    }
    
    // 检查已上传的分片
    const chunkDir = path.join(TEMP_DIR, fileHash);
    let uploadedChunks = [];
    
    if (await fs.pathExists(chunkDir)) {
      const chunks = await fs.readdir(chunkDir);
      uploadedChunks = chunks.map(chunk => chunk.replace('.chunk', ''));
    }
    
    res.json({ exists: false, uploadedChunks });
  } catch (err) {
    console.error('检查文件错误:', err);
    res.status(500).json({ error: err.message });
  }
});

/**
 * 2. 上传分片接口
 * 作用:接收前端上传的分片,存储到临时目录
 */
app.post('/upload-chunk', (req, res) => {
  const form = new multiparty.Form({ uploadDir: TEMP_DIR });
  
  form.parse(req, async (err, fields, files) => {
    if (err) {
      console.error('解析表单错误:', err);
      return res.status(500).json({ error: err.message });
    }
    
    try {
      const fileHash = fields.fileHash[0];
      const chunkHash = fields.chunkHash[0];
      const chunkFile = files.chunk[0];
      
      // 创建分片存储目录
      const chunkDir = path.join(TEMP_DIR, fileHash);
      await fs.ensureDir(chunkDir);
      
      // 移动分片到目标目录
      const destPath = path.join(chunkDir, `${chunkHash}.chunk`);
      await fs.move(chunkFile.path, destPath, { overwrite: true });
      
      res.json({ 
        success: true, 
        message: `分片 ${chunkHash} 上传成功`,
        chunkHash 
      });
    } catch (moveErr) {
      console.error('移动分片错误:', moveErr);
      res.status(500).json({ error: moveErr.message });
    }
  });
});

/**
 * 3. 合并分片接口
 * 作用:将所有分片按顺序合并为完整文件
 */
app.post('/merge-chunks', async (req, res) => {
  try {
    const { fileHash, fileName, totalChunks } = req.body;
    const chunkDir = path.join(TEMP_DIR, fileHash);
    const destPath = path.join(UPLOAD_DIR, fileName);
    
    // 检查分片目录是否存在
    if (!await fs.pathExists(chunkDir)) {
      return res.status(400).json({ error: '分片不存在' });
    }
    
    // 检查分片数量是否完整
    const chunks = await fs.readdir(chunkDir);
    if (chunks.length !== totalChunks) {
      return res.status(400).json({ 
        error: '分片不完整', 
        received: chunks.length, 
        total: totalChunks 
      });
    }
    
    // 按索引排序分片
    chunks.sort((a, b) => {
      const indexA = parseInt(a.split('-')[1].replace('.chunk', ''));
      const indexB = parseInt(b.split('-')[1].replace('.chunk', ''));
      return indexA - indexB;
    });
    
    // 合并分片
    const writeStream = fs.createWriteStream(destPath);
    for (const chunk of chunks) {
      const chunkPath = path.join(chunkDir, chunk);
      const chunkBuffer = await fs.readFile(chunkPath);
      await new Promise((resolve, reject) => {
        writeStream.write(chunkBuffer, (err) => {
          if (err) reject(err);
          else resolve();
        });
      });
    }
    
    // 完成合并,清理临时文件
    writeStream.end();
    await fs.remove(chunkDir);
    
    res.json({ 
      success: true, 
      message: '文件合并成功',
      filePath: destPath 
    });
  } catch (err) {
    console.error('合并分片错误:', err);
    res.status(500).json({ error: err.message });
  }
});

// 启动服务器
app.listen(port, () => {
  console.log(`后端服务已启动,端口: ${port}`);
  console.log(`上传文件存储目录: ${UPLOAD_DIR}`);
  console.log(`分片临时存储目录: ${TEMP_DIR}`);
});


网站公告

今日签到

点亮在社区的每一天
去签到