1. 项目背景与需求分析
在国企信息化建设项目中,经常需要处理大体积文件(如工程图纸、审计报告、视频资料等)的上传需求。传统ASP.NET文件上传方案存在三个核心痛点:
- 无法直接选择整个文件夹结构上传
- 大文件传输稳定性差
- 缺乏企业级的安全管控
某央企档案管理系统升级项目中,要求实现:
- 网页端可递归选择文件夹及其子目录
- 支持单文件超过2GB的稳定传输
- 传输过程需符合等保2.0要求
2. 技术方案设计
2.1 整体架构设计
采用分层处理方案:
[前端] ├─ 文件夹选择器(基于ActiveX/WebView2) ├─ 分片上传控件 └─ 断点续传管理器 [服务端] ├─ 接收中间件(Kestrel定制) ├─ 流式处理管道 └─ 审计日志模块2.2 关键技术选型
前端方案对比
| 方案 | 兼容性 | 文件夹选择 | 最大文件支持 |
|---|---|---|---|
| HTML5 File API | 现代浏览器 | × | 4GB |
| WebView2 | Win10+ | √ | 无限制 |
| ActiveX | IE Only | √ | 无限制 |
最终采用WebView2+HTML5混合方案,平衡兼容性与功能需求。
服务端配置关键参数
// Program.cs builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = 2147483648; // 2GB }); services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = long.MaxValue; });3. 核心实现步骤
3.1 文件夹选择功能实现
WebView2集成方案
// 初始化WebView2 async function initFilePicker() { const filePicker = await window.chrome.webview.showOpenFilePicker({ allowMultiple: true, types: [{ description: '所有文件', accept: {'*/*': ['.*']} }], excludeAcceptAllOption: false }); // 递归处理目录结构 const traverseDirectory = async (dirHandle) => { const entries = []; for await (const entry of dirHandle.values()) { if (entry.kind === 'file') { entries.push({ name: entry.name, handle: entry }); } else if (entry.kind === 'directory') { entries.push({ name: entry.name, children: await traverseDirectory(entry) }); } } return entries; }; return await traverseDirectory(filePicker[0]); }兼容性降级方案
// 针对IE的ActiveX方案 [ComVisible(true)] public class FolderBrowser { public string SelectedPath { get; set; } public bool ShowDialog() { var shell = new Shell32.Shell(); var folder = shell.BrowseForFolder(0, "选择上传目录", 0); if (folder != null) { SelectedPath = folder.Self.Path; return true; } return false; } }3.2 大文件分片上传
前端分片逻辑
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB分片 async function uploadFile(file, path) { const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const fileHash = await calculateMD5(file); for (let i = 0; i < totalChunks; i++) { const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE); const formData = new FormData(); formData.append('chunk', chunk); formData.append('chunkIndex', i); formData.append('totalChunks', totalChunks); formData.append('fileHash', fileHash); formData.append('relativePath', path); await fetch('/api/upload', { method: 'POST', body: formData, headers: { 'X-Resume-Token': getResumeToken() } }); } }服务端接收处理
[HttpPost] [RequestSizeLimit(2147483648)] public async Task<IActionResult> UploadChunk( IFormFile chunk, int chunkIndex, int totalChunks, string fileHash, string relativePath) { // 验证分片有效性 if (chunk == null || chunk.Length == 0) return BadRequest("无效分片"); // 创建临时存储目录 var tempDir = Path.Combine(_config["TempPath"], fileHash); Directory.CreateDirectory(tempDir); // 保存分片 var chunkPath = Path.Combine(tempDir, $"{chunkIndex}.part"); using (var stream = new FileStream(chunkPath, FileMode.Create)) { await chunk.CopyToAsync(stream); } // 检查是否全部完成 if (Directory.GetFiles(tempDir).Length == totalChunks) { await MergeChunks(tempDir, relativePath); } return Ok(new { chunkIndex }); }4. 企业级功能增强
4.1 安全控制措施
文件类型白名单验证
private static readonly Dictionary<string, List<byte[]>> _fileSignatures = new() { { ".docx", new List<byte[]> { new byte[] { 0x50, 0x4B, 0x03, 0x04 } } }, { ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } } }; bool IsValidFileType(IFormFile file) { using var reader = new BinaryReader(file.OpenReadStream()); var signatures = _fileSignatures[Path.GetExtension(file.FileName).ToLower()]; var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); return signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature)); }传输加密方案
// 采用国密SM4加密分片 public static byte[] EncryptChunk(byte[] data, string key) { using var sm4 = new SM4Engine(); var keyBytes = Encoding.UTF8.GetBytes(key); var iv = new byte[16]; sm4.Init(true, new KeyParameter(keyBytes)); sm4.ProcessBlock(data, 0, data, 0); return data; }4.2 性能优化策略
内存流池技术
public class MemoryStreamPool { private readonly ConcurrentBag<MemoryStream> _pool = new(); public MemoryStream Rent() { if (_pool.TryTake(out var stream)) return stream; return new MemoryStream(81920); // 预分配80KB } public void Return(MemoryStream stream) { stream.SetLength(0); _pool.Add(stream); } }异步处理管道
// Program.cs builder.Services.AddHostedService<UploadProcessingService>(); // 后台处理服务 public class UploadProcessingService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await _fileQueue.ProcessNextAsync(ProcessFile); await Task.Delay(100, stoppingToken); } } private async Task ProcessFile(UploadTask task) { using var processor = new FileProcessor(task); await processor.ValidateAsync(); await processor.StoreAsync(); await processor.WriteLogAsync(); } }5. 部署配置要点
5.1 IIS特殊配置
<!-- web.config --> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="2147483648" /> </requestFiltering> </security> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" /> </handlers> </system.webServer>5.2 Kestrel调优参数
builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxConcurrentConnections = 100; serverOptions.Limits.MaxConcurrentUpgradedConnections = 100; serverOptions.Limits.MaxRequestBodySize = 2147483648; serverOptions.Limits.MinRequestBodyDataRate = null; });6. 典型问题解决方案
6.1 大文件上传中断处理
断点续传实现
// 前端检查已上传分片 async function checkUploadProgress(fileHash) { const response = await fetch(`/api/upload/progress?hash=${fileHash}`); return await response.json(); } // 服务端记录 public class UploadProgressTracker { public Dictionary<string, List<int>> ProgressRecords { get; } = new(); public void RecordChunk(string fileHash, int chunkIndex) { if (!ProgressRecords.ContainsKey(fileHash)) ProgressRecords[fileHash] = new List<int>(); ProgressRecords[fileHash].Add(chunkIndex); } public List<int> GetProgress(string fileHash) { return ProgressRecords.TryGetValue(fileHash, out var progress) ? progress : new List<int>(); } }6.2 企业网络限制突破
分片并行上传
// 使用Promise.all实现可控并发 async function parallelUpload(file, maxConcurrent = 3) { const semaphore = new Semaphore(maxConcurrent); const uploadTasks = []; for (let i = 0; i < totalChunks; i++) { await semaphore.acquire(); uploadTasks.push( uploadChunk(i).finally(() => semaphore.release()) ); } await Promise.all(uploadTasks); } class Semaphore { constructor(max) { this.max = max; this.queue = []; } acquire() { return new Promise(resolve => { if (this.max > 0) { this.max--; resolve(); } else { this.queue.push(resolve); } }); } release() { if (this.queue.length > 0) { this.queue.shift()(); } else { this.max++; } } }7. 安全审计与监控
7.1 上传行为日志
public class UploadAuditMiddleware { public async Task InvokeAsync(HttpContext context) { var sw = Stopwatch.StartNew(); await _next(context); sw.Stop(); if (context.Request.Path.StartsWithSegments("/api/upload")) { _logger.LogInformation($"Upload {context.Response.StatusCode} | " + $"User: {context.User.Identity.Name} | " + $"Size: {context.Request.ContentLength} | " + $"Duration: {sw.ElapsedMilliseconds}ms"); await _auditService.RecordAsync( context.User.Identity.Name, context.Request.Headers["X-File-Name"], context.Request.ContentLength ?? 0, context.Connection.RemoteIpAddress); } } }7.2 实时流量监控
// 基于SignalR的监控看板 public class UploadHub : Hub { private readonly IUploadMonitor _monitor; public async Task<IEnumerable<UploadMetric>> GetMetrics() { return await _monitor.GetCurrentMetrics(); } public override async Task OnConnectedAsync() { await Groups.AddToGroupAsync(Context.ConnectionId, "Monitors"); await base.OnConnectedAsync(); } } // 前端监控面板 const connection = new signalR.HubConnectionBuilder() .withUrl("/uploadHub") .build(); connection.on("UpdateMetrics", metrics => { updateDashboard(metrics); }); connection.start();8. 实际部署经验
在某能源集团部署时遇到的关键问题及解决方案:
问题:跨国传输速度仅50KB/s
- 解决:启用Azure Front Door加速,传输速度提升至2MB/s
问题:AD域用户无法访问WebView2
- 解决:通过组策略推送注册表配置:
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge] "WebView2RuntimeInstallPolicy"=dword:00000001
- 解决:通过组策略推送注册表配置:
问题:防病毒软件锁定临时文件
- 解决:将临时目录加入杀毒软件白名单:
Add-MpPreference -ExclusionPath "D:\UploadTemp"
- 解决:将临时目录加入杀毒软件白名单:
9. 性能测试数据
在模拟国企典型环境中的测试结果(100并发):
| 文件大小 | 传统方案 | 本方案 | 提升 |
|---|---|---|---|
| 100MB | 45s | 12s | 275% |
| 1GB | 失败 | 68s | - |
| 10GB | 失败 | 11m | - |
关键指标:
- CPU利用率降低40%
- 内存消耗减少65%
- 失败率从12%降至0.3%
10. 扩展功能实现
10.1 文件自动归类
public class FileClassifier { public string Classify(string path) { var ext = Path.GetExtension(path).ToLower(); return ext switch { ".docx" or ".pdf" => "文档", ".dwg" or ".rvt" => "设计图纸", ".mp4" or ".avi" => "视频资料", _ => "其他" }; } }10.2 与OA系统集成
// 公文系统回调接口 [HttpPost] public async Task<IActionResult> NotifyOA([FromBody] OANotification notification) { var fileInfo = await _db.Files .Where(f => f.Hash == notification.FileHash) .FirstOrDefaultAsync(); if (fileInfo == null) return NotFound(); await _oaService.CreateDocument( notification.DocId, fileInfo.Name, fileInfo.Path, fileInfo.UploadUser); return Ok(); }在具体实施过程中,我们发现国企项目有三个特别需要注意的细节:
- 必须保留完整的文件树结构,不能扁平化处理
- 上传完成后需要生成符合档案管理规范的MD5校验文件
- 所有操作必须留痕且不可篡改
通过本文方案的实施,某省级国资平台已稳定运行3年,累计处理文件超过120TB,成功支撑了企业数字化转型中的核心文件传输需求。