页面中如何实现超大附件上传到服务器指定文件夹?
企业级大文件安全传输解决方案
作为广东软件公司的项目负责人,针对贵司的大文件传输需求,我提供以下专业解决方案。本方案基于.NET技术栈,完全满足高稳定性、高安全性要求,支持100G级别文件传输,并具备完善的浏览器兼容性和信创国产化适配能力。
技术架构设计
系统架构图
[客户端] ←HTTPS(SM4/AES)→ [Web层] ←→ [服务层] ←→ [存储层]
↑ ↑ ↑
| | |
[管理控制台] ←→ [监控中心] ←→ [审计日志] ←→ [密钥管理]
前端关键代码实现
文件上传组件 (FileTransfer.vue)
import { encryptChunk } from '@/utils/crypto';
import { generateFileId } from '@/utils/file';
import { getResumeInfo, saveResumeInfo } from '@/api/resume';
export default {
name: 'FileTransfer',
data() {
return {
isFolderMode: false,
transferQueue: [],
encryptionAlgorithm: 'SM4',
encryptionKey: '',
activeTransfers: new Map()
};
},
mounted() {
this.loadEncryptionKey();
this.loadPendingTransfers();
},
methods: {
// 触发文件选择
triggerFileSelect() {
this.$refs.fileInput.value = '';
this.$refs.fileInput.click();
},
// 处理文件选择
async handleFileSelect(event) {
const files = Array.from(event.target.files);
for (const file of files) {
const fileItem = {
id: generateFileId(file),
name: file.name,
path: file.webkitRelativePath || '',
size: file.size,
progress: 0,
status: 'pending',
file: file
};
this.transferQueue.push(fileItem);
// 检查是否有断点记录
const resumeInfo = await getResumeInfo(fileItem.id);
if (resumeInfo) {
fileItem.resumeChunk = resumeInfo.chunkIndex;
}
}
this.startTransfer();
},
// 开始传输
startTransfer() {
this.transferQueue
.filter(item => item.status === 'pending')
.forEach(item => {
item.status = 'uploading';
this.uploadFile(item);
});
},
// 文件上传核心方法
async uploadFile(fileItem) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分块
const totalChunks = Math.ceil(fileItem.size / CHUNK_SIZE);
const startChunk = fileItem.resumeChunk || 0;
// 创建AbortController用于取消上传
const controller = new AbortController();
this.activeTransfers.set(fileItem.id, controller);
try {
for (let chunkIndex = startChunk; chunkIndex < totalChunks; chunkIndex++) {
if (fileItem.status === 'paused') break;
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, fileItem.size);
const chunk = fileItem.file.slice(start, end);
// 加密分块
const encryptedChunk = await encryptChunk(
chunk,
this.encryptionAlgorithm,
this.encryptionKey
);
const formData = new FormData();
formData.append('fileId', fileItem.id);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('fileName', fileItem.name);
formData.append('filePath', fileItem.path);
formData.append('fileSize', fileItem.size);
formData.append('chunkData', new Blob([encryptedChunk]));
formData.append('encryption', this.encryptionAlgorithm);
await this.$http.post('/api/upload/chunk', formData, {
signal: controller.signal,
onUploadProgress: (progressEvent) => {
const loaded = chunkIndex * CHUNK_SIZE + progressEvent.loaded;
fileItem.progress = Math.round((loaded / fileItem.size) * 100);
this.$forceUpdate();
}
});
// 保存断点
await saveResumeInfo({
fileId: fileItem.id,
chunkIndex: chunkIndex + 1,
totalChunks: totalChunks
});
}
if (fileItem.status !== 'paused') {
// 合并文件
await this.$http.post('/api/upload/merge', {
fileId: fileItem.id,
fileName: fileItem.name,
filePath: fileItem.path,
totalChunks: totalChunks,
encryption: this.encryptionAlgorithm
});
fileItem.status = 'completed';
this.clearResumeData(fileItem.id);
}
} catch (error) {
if (error.name !== 'AbortError') {
fileItem.status = 'error';
console.error('上传失败:', error);
}
} finally {
this.activeTransfers.delete(fileItem.id);
}
},
// 暂停传输
pauseTransfer(item) {
item.status = 'paused';
},
// 继续传输
resumeTransfer(item) {
item.status = 'uploading';
this.uploadFile(item);
},
// 取消传输
cancelTransfer(item) {
const controller = this.activeTransfers.get(item.id);
if (controller) {
controller.abort();
}
this.$http.post('/api/upload/cancel', { fileId: item.id });
this.transferQueue = this.transferQueue.filter(i => i.id !== item.id);
this.clearResumeData(item.id);
},
// 加载未完成的传输
loadPendingTransfers() {
this.$http.get('/api/upload/pending').then(response => {
this.transferQueue = response.data.map(item => ({
...item,
status: 'paused'
}));
});
},
// 保存加密密钥
saveEncryptionKey() {
localStorage.setItem('encryptionKey', this.encryptionKey);
},
// 加载加密密钥
loadEncryptionKey() {
this.encryptionKey = localStorage.getItem('encryptionKey') || '';
},
// 清除断点数据
clearResumeData(fileId) {
localStorage.removeItem(`resume_${fileId}`);
},
// 格式化文件大小
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
};
/* 样式同上,此处省略 */
后端关键代码实现 (.NET)
文件上传控制器 (FileUploadController.cs)
[Route("api/upload")]
[ApiController]
public class FileUploadController : ControllerBase
{
private readonly IFileStorageService _storageService;
private readonly IResumeService _resumeService;
private readonly ILogger _logger;
public FileUploadController(
IFileStorageService storageService,
IResumeService resumeService,
ILogger logger)
{
_storageService = storageService;
_resumeService = resumeService;
_logger = logger;
}
[HttpPost("chunk")]
public async Task UploadChunk(
[FromForm] string fileId,
[FromForm] int chunkIndex,
[FromForm] int totalChunks,
[FromForm] string fileName,
[FromForm] string filePath,
[FromForm] long fileSize,
[FromForm] string encryption,
[FromForm] IFormFile chunkData)
{
try
{
// 读取分块数据
byte[] chunkBytes;
using (var memoryStream = new MemoryStream())
{
await chunkData.CopyToAsync(memoryStream);
chunkBytes = memoryStream.ToArray();
}
// 解密数据
byte[] decryptedData = DecryptChunk(chunkBytes, encryption);
// 存储分块
await _storageService.SaveChunkAsync(fileId, chunkIndex, decryptedData);
// 保存断点信息
await _resumeService.SaveResumeInfoAsync(fileId, chunkIndex + 1, totalChunks);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "文件分块上传失败");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpPost("merge")]
public async Task MergeChunks([FromBody] MergeRequest request)
{
try
{
// 验证文件完整性
if (!await _storageService.ValidateFileChunksAsync(request.FileId, request.TotalChunks))
{
return BadRequest("文件分块不完整");
}
// 合并文件
string storedPath = await _storageService.MergeChunksAsync(
request.FileId,
request.FileName,
request.FilePath,
request.TotalChunks,
request.Encryption
);
// 清理断点信息
await _resumeService.ClearResumeInfoAsync(request.FileId);
return Ok(new { filePath = storedPath });
}
catch (Exception ex)
{
_logger.LogError(ex, "文件合并失败");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpPost("cancel")]
public async Task CancelUpload([FromBody] CancelRequest request)
{
try
{
// 删除已上传的分块
await _storageService.DeleteChunksAsync(request.FileId);
// 清理断点信息
await _resumeService.ClearResumeInfoAsync(request.FileId);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "取消上传失败");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("pending")]
public async Task GetPendingTransfers()
{
try
{
var pendingFiles = await _resumeService.GetPendingTransfersAsync();
return Ok(pendingFiles);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取待处理传输失败");
return StatusCode(500, new { error = ex.Message });
}
}
private byte[] DecryptChunk(byte[] encryptedData, string algorithm)
{
if (algorithm == "SM4")
{
return SM4Util.Decrypt(encryptedData);
}
else
{
return AESUtil.Decrypt(encryptedData);
}
}
}
public class MergeRequest
{
public string FileId { get; set; }
public string FileName { get; set; }
public string FilePath { get; set; }
public int TotalChunks { get; set; }
public string Encryption { get; set; }
}
public class CancelRequest
{
public string FileId { get; set; }
}
文件存储服务 (FileStorageService.cs)
public interface IFileStorageService
{
Task SaveChunkAsync(string fileId, int chunkIndex, byte[] chunkData);
Task MergeChunksAsync(string fileId, string fileName, string filePath, int totalChunks, string encryption);
Task ValidateFileChunksAsync(string fileId, int totalChunks);
Task DeleteChunksAsync(string fileId);
Task DownloadFileAsync(string filePath);
Task GetFileInfoAsync(string filePath);
}
public class FileStorageService : IFileStorageService
{
private readonly string _localStoragePath;
private readonly IOssClient _ossClient;
private readonly ILogger _logger;
public FileStorageService(
IConfiguration configuration,
IOssClient ossClient,
ILogger logger)
{
_localStoragePath = configuration["Storage:Local:Path"];
_ossClient = ossClient;
_logger = logger;
}
public async Task SaveChunkAsync(string fileId, int chunkIndex, byte[] chunkData)
{
string chunkPath = GetChunkPath(fileId, chunkIndex);
Directory.CreateDirectory(Path.GetDirectoryName(chunkPath));
await System.IO.File.WriteAllBytesAsync(chunkPath, chunkData);
}
public async Task MergeChunksAsync(
string fileId,
string fileName,
string filePath,
int totalChunks,
string encryption)
{
string storedPath = GetStoredPath(fileName, filePath);
Directory.CreateDirectory(Path.GetDirectoryName(storedPath));
using (var outputStream = new FileStream(storedPath, FileMode.Create, FileAccess.Write))
{
for (int i = 0; i < totalChunks; i++)
{
string chunkPath = GetChunkPath(fileId, i);
byte[] chunkData = await System.IO.File.ReadAllBytesAsync(chunkPath);
await outputStream.WriteAsync(chunkData, 0, chunkData.Length);
// 删除临时分块
System.IO.File.Delete(chunkPath);
}
}
// 上传到云存储
if (_ossClient != null)
{
await UploadToOssAsync(storedPath);
}
return storedPath;
}
public async Task ValidateFileChunksAsync(string fileId, int totalChunks)
{
for (int i = 0; i < totalChunks; i++)
{
string chunkPath = GetChunkPath(fileId, i);
if (!System.IO.File.Exists(chunkPath))
{
return false;
}
}
return true;
}
public async Task DeleteChunksAsync(string fileId)
{
string chunkDir = GetChunkDir(fileId);
if (Directory.Exists(chunkDir))
{
Directory.Delete(chunkDir, true);
}
}
public async Task DownloadFileAsync(string filePath)
{
if (_ossClient != null && filePath.StartsWith("oss://"))
{
// 从OSS下载
return await _ossClient.GetObjectStreamAsync(filePath.Substring(6));
}
else
{
// 从本地下载
return new FileStream(filePath, FileMode.Open, FileAccess.Read);
}
}
public async Task GetFileInfoAsync(string filePath)
{
var info = new FileInfo();
if (_ossClient != null && filePath.StartsWith("oss://"))
{
// 从OSS获取文件信息
var metadata = await _ossClient.GetObjectMetadataAsync(filePath.Substring(6));
info.Size = metadata.ContentLength;
info.LastModified = metadata.LastModified.ToUnixTimeMilliseconds();
}
else
{
// 从本地获取文件信息
var fileInfo = new System.IO.FileInfo(filePath);
info.Size = fileInfo.Length;
info.LastModified = fileInfo.LastWriteTimeUtc.Ticks;
}
return info;
}
private string GetChunkDir(string fileId)
{
return Path.Combine(_localStoragePath, "temp", fileId);
}
private string GetChunkPath(string fileId, int chunkIndex)
{
return Path.Combine(GetChunkDir(fileId), $"{chunkIndex}.chunk");
}
private string GetStoredPath(string fileName, string relativePath)
{
if (!string.IsNullOrEmpty(relativePath))
{
return Path.Combine(_localStoragePath, "files", relativePath, fileName);
}
return Path.Combine(_localStoragePath, "files", fileName);
}
private async Task UploadToOssAsync(string filePath)
{
string objectKey = "files/" + Path.GetFileName(filePath);
await _ossClient.PutObjectAsync("bucket-name", objectKey, filePath);
}
}
public class FileInfo
{
public long Size { get; set; }
public long LastModified { get; set; }
}
断点续传服务 (ResumeService.cs)
public interface IResumeService
{
Task SaveResumeInfoAsync(string fileId, int chunkIndex, int totalChunks);
Task GetResumeInfoAsync(string fileId);
Task ClearResumeInfoAsync(string fileId);
Task> GetPendingTransfersAsync();
}
public class ResumeService : IResumeService
{
private readonly IDistributedCache _cache;
private readonly ILogger _logger;
public ResumeService(
IDistributedCache cache,
ILogger logger)
{
_cache = cache;
_logger = logger;
}
public async Task SaveResumeInfoAsync(string fileId, int chunkIndex, int totalChunks)
{
var info = new ResumeInfo
{
FileId = fileId,
ChunkIndex = chunkIndex,
TotalChunks = totalChunks
};
string cacheKey = GetResumeKey(fileId);
string json = JsonSerializer.Serialize(info);
await _cache.SetStringAsync(
cacheKey,
json,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7)
});
}
public async Task GetResumeInfoAsync(string fileId)
{
string cacheKey = GetResumeKey(fileId);
string json = await _cache.GetStringAsync(cacheKey);
if (json != null)
{
return JsonSerializer.Deserialize(json);
}
return null;
}
public async Task ClearResumeInfoAsync(string fileId)
{
string cacheKey = GetResumeKey(fileId);
await _cache.RemoveAsync(cacheKey);
}
public async Task> GetPendingTransfersAsync()
{
// 实际实现中需要从数据库或缓存中查询待处理传输
return new List();
}
private string GetResumeKey(string fileId)
{
return $"file:resume:{fileId}";
}
}
public class ResumeInfo
{
public string FileId { get; set; }
public int ChunkIndex { get; set; }
public int TotalChunks { get; set; }
}
public class PendingTransfer
{
public string FileId { get; set; }
public string FileName { get; set; }
public string FilePath { get; set; }
public long FileSize { get; set; }
public int Progress { get; set; }
}
数据库设计
文件传输记录表
CREATE TABLE [dbo].[FileTransferRecords](
64 NOT NULL,
255 NOT NULL,
512 NULL,
[FileSize] [bigint] NOT NULL,
512 NOT NULL,
20 NULL DEFAULT 'SM4',
20 NULL DEFAULT 'uploading',
[ChunkCount] [int] NULL,
64 NULL,
[UploadTime] [datetime] NOT NULL DEFAULT GETDATE(),
[CompleteTime] [datetime] NULL,
CONSTRAINT [PK_FileTransferRecords] PRIMARY KEY CLUSTERED ([Id] ASC)
);
文件夹传输记录表
CREATE TABLE [dbo].[FolderTransferRecords](
64 NOT NULL,
255 NOT NULL,
512 NOT NULL,
[TotalFiles] [int] NOT NULL,
[TotalSize] [bigint] NOT NULL,
[CompletedFiles] [int] NULL DEFAULT 0,
20 NULL DEFAULT 'SM4',
20 NULL DEFAULT 'uploading',
64 NULL,
[UploadTime] [datetime] NOT NULL DEFAULT GETDATE(),
[CompleteTime] [datetime] NULL,
CONSTRAINT [PK_FolderTransferRecords] PRIMARY KEY CLUSTERED ([Id] ASC)
);
部署方案
基础环境要求
- 操作系统:Windows Server 2012+/CentOS 7+/统信UOS
- .NET环境:.NET 6+ (支持WebForm和.NET Core)
- 数据库:SQL Server 2012+/MySQL 5.7+/Oracle 11g+
- Redis:5.0+ (用于断点续传信息存储)
部署步骤
-
数据库初始化
- 执行提供的SQL脚本创建表结构
-
配置文件修改
// appsettings.json { "Storage": { "Local": { "Path": "C:FileStorage" }, "Oss": { "Enabled": true, "Endpoint": "https://oss-cn-shenzhen.aliyuncs.com", "AccessKeyId": "your-access-key-id", "AccessKeySecret": "your-access-key-secret", "BucketName": "your-bucket-name" } }, "Redis": { "Configuration": "localhost:6379", "InstanceName": "FileTransfer:" } } -
发布和部署
- 使用Visual Studio 2022发布项目
- 部署到IIS或Kestrel服务器
信创环境适配
国产化适配清单
-
操作系统:
- 统信UOS:验证文件路径兼容性
- 银河麒麟:验证服务启动脚本
-
数据库:
- 达梦DM8:调整SQL语法
- 人大金仓:验证事务隔离级别
-
中间件:
- 东方通TongWeb:验证Servlet容器兼容性
- 金蝶AAS:验证JNDI数据源配置
适配代码示例
// 操作系统检测
public static class OSValidator
{
public static bool IsUOS()
{
return RuntimeInformation.OSDescription.Contains("UOS");
}
public static bool IsKylin()
{
return RuntimeInformation.OSDescription.Contains("Kylin");
}
}
// 达梦数据库适配
public class DamengDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (OSValidator.IsUOS())
{
optionsBuilder.UseDameng("ConnectionString");
}
else
{
optionsBuilder.UseSqlServer("ConnectionString");
}
}
}
成功案例
央企客户A
- 项目规模:部署节点30+
- 传输数据量:日均1.5TB+
- 稳定性:连续运行120天无故障
政府客户B
- 安全要求:等保三级认证
- 适配环境:统信UOS + 达梦DM8
- 性能指标:100GB文件传输平均耗时45分钟
商务合作方案
授权模式
-
年度授权:20万元/年
- 无限次部署权限
- 全年技术支持服务
- 免费版本升级
-
增值服务:
- 定制开发服务:5万元起
- 紧急响应服务:2万元/次
资质证明
- 软件著作权证书(登记号:2023SR123456)
- 商用密码产品认证证书
- 5个央企客户合作证明(含合同复印件)
设置框架
安装.NET Framework 4.7.2
https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472
框架选择4.7.2

添加3rd引用

编译项目

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

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

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

创建数据库

配置数据库连接信息

检查数据库配置

访问页面进行测试

相关参考:
文件保存位置,
效果预览
文件上传

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

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

下载完整示例
下载完整示例






