政府招投标项目大文件传输解决方案探索

作为山东济南一家软件公司项目负责人,近期我们正全身心投入一个政府招投标项目。在项目推进过程中,大文件传输需求成为了一大关键挑战,必须找到一套契合项目各方面要求的解决方案。

一、项目需求剖析

(一)文件传输规模与功能

本次项目要求支持 50G 左右的大文件传输,涵盖文件和文件夹的上传与下载功能。尤为关键的是,文件夹传输时需完整保留其层级结构,确保数据的完整性和准确性。

(二)信创国产化适配

由于是政府项目,信息安全至关重要,必须全面支持信创国产化环境。具体包括:

  1. 操作系统:统信 UOS、中标麒麟、银河麒麟。
  2. 浏览器:主流浏览器以及信创国产浏览器,如龙芯浏览器、红莲花浏览器、奇安信安全浏览器。
  3. 数据库:主流的 SQL Server、MySQL、Oracle 以及国产化的达梦、人大金仓。

(三)技术架构与部署

后端采用.NET Core 技术栈,前端基于 vue2 - cli 框架。项目部署于内网环境,需支持私有部署,以保障数据的安全性和隐私性。

(四)授权与成本考量

公司客户主要集中在政府和军工单位,考虑到项目数量众多,每年约 2000 + 个,单套授权方式不仅成本高昂,而且管理繁琐。因此,领导决定采购产品源代码,由公司研发部门自主负责后续开发与维护,同时满足产品部门的自研需求。

二、开源组件调研困境

在项目初期,我们对市场上的开源组件进行了广泛调研,但结果不尽如人意。以百度开源的大文件上传组件 WebUploader 为例,该组件目前已经停止更新,缺乏技术支持。我们曾尝试联系开发人员,但发出的消息一个多月都未得到任何回应。其他开源组件也存在类似问题,没有可靠的技术支持渠道,一旦遇到问题无法及时联系到作者解决,这给项目的顺利推进带来了极大风险,因此我们决定重新寻找更合适的解决方案。

三、解决方案探索与部分代码实现

(一)整体架构设计思路

考虑到项目需求和技术栈,我们计划基于.NET Core 和 vue2 - cli 构建一套自定义的大文件传输解决方案。后端负责文件分片处理、存储管理以及与数据库的交互;前端实现文件分片上传、下载以及进度显示等功能。

(二)后端实现(.NET Core)

1. 文件分片接收与合并
using Microsoft.AspNetCore.Mvc;
using System.IO;

[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
    private readonly string _uploadFolder = "Uploads";

    public FileUploadController()
    {
        if (!Directory.Exists(_uploadFolder))
        {
            Directory.CreateDirectory(_uploadFolder);
        }
    }

    [HttpPost("upload")]
    public IActionResult Upload([FromForm] FileUploadModel model)
    {
        string filePath = Path.Combine(_uploadFolder, model.FileName);
        string tempFilePath = filePath + ".tmp";

        // 如果是第一个分片,创建临时文件;否则追加到临时文件
        if (model.ChunkIndex == 0)
        {
            using (var fileStream = new FileStream(tempFilePath, FileMode.Create))
            {
                fileStream.Write(model.FileData, 0, model.FileData.Length);
            }
        }
        else
        {
            using (var fileStream = new FileStream(tempFilePath, FileMode.Append))
            {
                fileStream.Write(model.FileData, 0, model.FileData.Length);
            }
        }

        // 如果是最后一个分片,将临时文件重命名为正式文件
        if (model.IsLastChunk)
        {
            System.IO.File.Move(tempFilePath, filePath);
            return Ok(new { message = "File uploaded successfully" });
        }

        return Ok(new { message = "Chunk uploaded successfully" });
    }
}

public class FileUploadModel
{
    public string FileName { get; set; }
    public byte[] FileData { get; set; }
    public int ChunkIndex { get; set; }
    public bool IsLastChunk { get; set; }
}
2. 数据库记录文件信息
using Microsoft.EntityFrameworkCore;

public class FileContext : DbContext
{
    public DbSet Files { get; set; }

    public FileContext(DbContextOptions options) : base(options)
    {
    }
}

public class FileInfo
{
    public int Id { get; set; }
    public string FileName { get; set; }
    public string FilePath { get; set; }
    public long FileSize { get; set; }
    // 其他文件相关信息字段
}

在项目启动时,根据配置的数据库类型(SQL Server、MySQL、Oracle、达梦、人大金仓),配置相应的数据库连接字符串和 EF Core 提供程序。例如,对于 MySQL 数据库:

// 在 Program.cs 中配置 MySQL 数据库连接
var builder = WebApplication.CreateBuilder(args);

// 添加 MySQL EF Core 提供程序
builder.Services.AddDbContext(options =>
    options.UseMySql(builder.Configuration.GetConnectionString("MySQLConnection"), 
    ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("MySQLConnection"))));

(三)前端实现(vue2 - cli)

1. 文件分片上传组件



export default {
  data() {
    return {
      selectedFiles: [],
      uploadProgress: 0
    };
  },
  methods: {
    handleFileChange(event) {
      this.selectedFiles = Array.from(event.target.files);
    },
    async uploadFiles() {
      for (const file of this.selectedFiles) {
        const chunkSize = 5 * 1024 * 1024; // 5MB 分片大小
        const totalChunks = Math.ceil(file.size / chunkSize);
        let uploadedChunks = 0;

        for (let i = 0; i < totalChunks; i++) {
          const start = i * chunkSize;
          const end = Math.min(start + chunkSize, file.size);
          const chunk = file.slice(start, end);

          const formData = new FormData();
          formData.append('FileName', file.name);
          formData.append('FileData', chunk);
          formData.append('ChunkIndex', i);
          formData.append('IsLastChunk', i === totalChunks - 1);

          try {
            await this.$http.post('/api/fileupload/upload', formData, {
              onUploadProgress: (progressEvent) => {
                uploadedChunks++;
                this.uploadProgress = Math.round((uploadedChunks / totalChunks) * 100);
              }
            });
          } catch (error) {
            console.error('上传分片失败:', error);
            break;
          }
        }
      }
    }
  }
};

2. 文件夹层级结构处理

在前端,当用户选择文件夹上传时,需要通过递归的方式遍历文件夹中的所有文件和子文件夹,并为每个文件生成相对路径,以便在传输到后端后能够恢复文件夹的层级结构。

function traverseFolder(entry, relativePath = '') {
  return new Promise((resolve, reject) => {
    const results = [];
    if (entry.isFile) {
      entry.file(file => {
        // 这里可以对文件进行一些处理,比如设置文件的相对路径
        file.relativePath = relativePath + file.name;
        results.push(file);
        resolve(results);
      });
    } else if (entry.isDirectory) {
      const dirReader = entry.createReader();
      dirReader.readEntries(entries => {
        const promises = [];
        for (const subEntry of entries) {
          const newRelativePath = relativePath + entry.name + '/';
          promises.push(traverseFolder(subEntry, newRelativePath));
        }
        Promise.all(promises).then(subResults => {
          subResults.forEach(subResult => {
            results.push(...subResult);
          });
          resolve(results);
        }).catch(reject);
      }, reject);
    }
  });
}

handleFileChange 方法中调用 traverseFolder 方法来处理文件夹上传:

handleFileChange(event) {
  const files = Array.from(event.target.files);
  const folderPromises = [];
  for (const file of files) {
    if (file.webkitRelativePath) {
      // 如果是文件夹中的文件,通过 webkitRelativePath 获取相对路径信息
      // 这里可以进一步优化处理文件夹层级结构的逻辑
      const entry = {
        isFile: true,
        file: (callback) => {
          callback(file);
        }
      };
      // 简单模拟,实际需要根据 webkitRelativePath 构建更完整的层级结构处理
      file.relativePath = file.webkitRelativePath;
    } else {
      // 如果是单独的文件,直接处理
      continue;
    }
  }
  // 更完整的文件夹处理示例
  if (files.length > 0 && files[0].webkitGetAsEntry) {
    const entry = files[0].webkitGetAsEntry();
    if (entry && entry.isDirectory) {
      traverseFolder(entry).then(folderFiles => {
        this.selectedFiles = this.selectedFiles.concat(folderFiles);
      });
    }
  }
},

四、后续计划与展望

目前,我们已经完成了大文件传输解决方案的基本架构设计和部分代码实现。接下来,我们将重点进行以下工作:

  1. 全面测试:在不同信创国产化操作系统和浏览器上进行全面测试,确保文件传输功能的稳定性和兼容性。
  2. 性能优化:对大文件传输过程进行性能优化,提高传输速度和效率,减少传输时间。
  3. 安全加固:加强信息安全防护,对文件传输过程进行加密处理,防止数据泄露。
  4. 完善功能:根据项目实际需求,进一步完善文件夹层级结构处理、文件管理等功能。

通过本次探索和实现,我们有信心为政府招投标项目提供一套满足各方面要求的大文件传输解决方案,为公司赢得更多项目机会,同时也为信创国产化在软件领域的应用贡献一份力量。

设置框架

安装.NET Framework 4.7.2
https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472
框架选择4.7.2
Alt

添加3rd引用

Alt

编译项目

Alt

NOSQL

NOSQL无需任何配置可直接访问页面进行测试
Alt

SQL

使用IIS
大文件上传测试推荐使用IIS以获取更高性能。
Alt

使用IIS Express

小文件上传测试可以使用IIS Express
Alt

创建数据库

Alt

配置数据库连接信息

Alt

检查数据库配置

Alt

访问页面进行测试

Alt
相关参考:
文件保存位置

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

批量下载

支持文件批量下载
批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
下载续传

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
文件夹下载

下载完整示例

下载完整示例

更多推荐