1. 医疗系统中.NET MVC文件夹上传功能实现方案
在医疗信息化系统中,文件上传功能是电子病历、影像资料管理等核心业务的基础支撑。传统单文件上传已无法满足实际需求,特别是当需要批量上传包含多层目录结构的检查报告、影像序列时。本文将详细介绍基于.NET MVC框架的文件夹上传完整解决方案。
医疗行业对文件上传有特殊要求:需保留原始目录结构(如DICOM影像序列)、支持大文件传输(如CT/MRI原始数据)、确保数据安全(加密传输)并兼容老旧系统(如仍在使用IE8的医疗设备工作站)。
1.1 系统架构设计
医疗文件上传系统采用分层架构:
- 表现层:基于WebUploader的HTML5上传界面,提供文件夹选择、上传进度展示
- 业务逻辑层:.NET MVC控制器处理分片上传、合并请求
- 数据访问层:文件分片临时存储、最终文件持久化(本地存储+阿里云OSS双备份)
- 安全层:前端SM4/AES加密、传输HTTPS加密、存储加密
mermaid复制graph TD
A[浏览器] -->|加密分片上传| B[MVC控制器]
B --> C[分片临时存储]
C --> D[分片合并]
D --> E[本地存储]
D --> F[OSS云存储]
E --> G[数据库记录]
F --> G
1.2 关键技术选型
1.2.1 前端技术栈
- WebUploader:支持HTML5与Flash双模式,兼容IE8+
- CryptoJS:实现前端文件加密(SM4/AES)
- FileSystem API:用于文件夹递归遍历(Chrome/Firefox)
- Flash:IE8文件夹访问后备方案
1.2.2 后端技术栈
- ASP.NET MVC 5:路由控制、请求处理
- Entity Framework:上传记录持久化
- Aliyun OSS SDK:云端存储集成
- Quartz.NET:定时清理临时分片
2. 核心功能实现细节
2.1 文件夹上传前端实现
医疗场景需要保持DICOM影像的目录结构,关键实现如下:
javascript复制// 递归处理文件夹内容
function traverseDirectory(entries, relativePath = '') {
entries.forEach(entry => {
if (entry.isDirectory) {
const dirReader = entry.createReader()
dirReader.readEntries(entries => {
traverseDirectory(entries, `${relativePath}${entry.name}/`)
})
} else {
entry.file(file => {
file.relativePath = relativePath // 保留相对路径
uploader.addFiles(file)
})
}
})
}
// IE8特殊处理
if (isIE8) {
document.getElementById('ie8-folder-input').addEventListener('change', function() {
const files = this.files
for (let i = 0; i < files.length; i++) {
files[i].relativePath = this.value.split('\\').slice(0, -1).join('/') + '/'
uploader.addFiles(files[i])
}
})
}
关键点:通过HTML5 Directory API获取完整目录结构,IE8下使用Flash或通过input的webkitdirectory属性模拟(需用户手动选择整个文件夹)
2.2 分片上传与断点续传
医疗影像文件通常较大(单文件可达GB级),必须实现可靠的分片上传:
csharp复制// MVC控制器处理分片上传
[HttpPost]
public ActionResult UploadChunk(string fileMd5, int chunkIndex, int totalChunks,
string fileName, string relativePath)
{
var chunkFile = Path.Combine(Config.TempPath, fileMd5, chunkIndex.ToString());
// 保存分片
Request.Files[0].SaveAs(chunkFile);
// 检查是否所有分片已上传
var chunkDir = new DirectoryInfo(Path.Combine(Config.TempPath, fileMd5));
if (chunkDir.GetFiles().Length == totalChunks)
{
MergeChunks(fileMd5, fileName, relativePath);
}
return Json(new { success = true });
}
private void MergeChunks(string fileMd5, string fileName, string relativePath)
{
// 创建目标目录(保留原始结构)
var destPath = Path.Combine(Config.UploadRoot, relativePath.Trim('/'));
Directory.CreateDirectory(destPath);
// 合并文件
using (var destStream = new FileStream(Path.Combine(destPath, fileName), FileMode.Create))
{
foreach (var chunkFile in Directory.GetFiles(Path.Combine(Config.TempPath, fileMd5))
.OrderBy(f => int.Parse(Path.GetFileName(f))))
{
using (var sourceStream = File.OpenRead(chunkFile))
{
sourceStream.CopyTo(destStream);
}
}
}
// 上传到OSS
UploadToOSS(Path.Combine(destPath, fileName),
Path.Combine("medical", relativePath, fileName));
// 清理临时文件
Directory.Delete(Path.Combine(Config.TempPath, fileMd5), true);
}
2.3 医疗数据安全传输
为满足HIPAA等医疗数据安全要求,实现端到端加密:
javascript复制// 前端加密(使用SM4国密算法)
function encryptChunk(chunk, key) {
const sm4 = new SM4({
key: CryptoJS.enc.Utf8.parse(key),
mode: 'cbc',
iv: CryptoJS.enc.Utf8.parse('0123456789ABCDEF')
})
return sm4.encrypt(chunk)
}
// 后端解密(C#)
public static byte[] SM4Decrypt(byte[] encryptedData, string key)
{
using (var sm4 = new SM4CryptoServiceProvider())
{
sm4.Key = Encoding.UTF8.GetBytes(key);
sm4.IV = Encoding.UTF8.GetBytes("0123456789ABCDEF");
using (var ms = new MemoryStream())
using (var cs = new CryptoStream(ms, sm4.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(encryptedData, 0, encryptedData.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
}
}
3. 医疗场景特殊处理
3.1 DICOM影像序列处理
医学影像通常以序列形式存储,需要特殊处理:
csharp复制// 识别DICOM文件并提取元数据
public ActionResult UploadDicomSeries(string studyUid)
{
var files = Request.Files;
var seriesDict = new Dictionary<string, List<HttpPostedFileBase>>();
foreach (HttpPostedFileBase file in files)
{
// 读取DICOM文件头
var dicomFile = DicomFile.Open(file.InputStream);
var seriesUid = dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID);
if (!seriesDict.ContainsKey(seriesUid))
seriesDict[seriesUid] = new List<HttpPostedFileBase>();
seriesDict[seriesUid].Add(file);
}
// 按序列存储
foreach (var series in seriesDict)
{
var seriesPath = Path.Combine(Config.DicomRoot, studyUid, series.Key);
Directory.CreateDirectory(seriesPath);
foreach (var file in series.Value)
{
file.SaveAs(Path.Combine(seriesPath, file.FileName));
}
}
return Json(new { success = true });
}
3.2 与HIS系统集成
上传完成后触发HIS系统对接:
csharp复制private void NotifyHIS(string patientId, string studyUid, string[] filePaths)
{
var hisService = new HISWebService(Config.HISEndpoint);
var result = hisService.RegisterStudy(
new StudyInfo {
PatientID = patientId,
StudyInstanceUID = studyUid,
Files = filePaths.Select(p => new StudyFile {
Path = p,
Size = new FileInfo(p).Length,
Type = Path.GetExtension(p).ToUpper()
}).ToList()
});
if (!result.Success)
throw new Exception("HIS系统登记失败: " + result.ErrorMessage);
}
4. 性能优化方案
4.1 上传加速策略
-
动态分片大小调整:
javascript复制// 根据网络状况调整分片大小 function getOptimalChunkSize() { const connectionSpeed = navigator.connection?.downlink || 5; // Mbps return Math.min( Math.max(1, Math.floor(connectionSpeed * 1024 * 0.8)), // 80%带宽利用率 20 // 最大20MB ) * 1024 * 1024; } -
并行上传控制:
csharp复制// Web.config调整最大请求大小 <system.web> <httpRuntime maxRequestLength="2147483647" executionTimeout="3600" /> </system.web> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="4294967295" /> </requestFiltering> </security> </system.webServer>
4.2 医疗数据存储优化
-
冷热数据分离存储:
- 热数据(最近3个月):本地SSD存储
- 温数据(3-12个月):阿里云OSS标准存储
- 冷数据(1年以上):阿里云OSS归档存储
-
智能预加载:
csharp复制// 根据患者预约信息预加载历史影像 public void PreloadPatientData(string patientId) { var studies = _db.Studies .Where(s => s.PatientID == patientId && s.Status == "Scheduled") .ToList(); foreach (var study in studies) { var files = _db.StudyFiles .Where(f => f.StudyID == study.ID) .Select(f => f.OSSKey) .ToList(); Task.Run(() => { foreach (var key in files) { var tempPath = Path.Combine(Config.PreloadCache, key); if (!File.Exists(tempPath)) { _ossClient.GetObject(new GetObjectRequest(Config.BucketName, key), tempPath); } } }); } }
5. 异常处理与日志监控
5.1 医疗数据上传审计
csharp复制// 上传审计日志
public class UploadAuditLogger
{
public static void LogUpload(string userId, string patientId, string[] files)
{
using (var db = new MedicalDbContext())
{
db.UploadLogs.Add(new UploadLog {
UserID = userId,
PatientID = patientId,
FileCount = files.Length,
TotalSize = files.Sum(f => new FileInfo(f).Length),
IPAddress = HttpContext.Current.Request.UserHostAddress,
UploadTime = DateTime.Now
});
db.SaveChanges();
}
// 同步到ELK
var logEvent = new {
timestamp = DateTime.UtcNow,
eventType = "file_upload",
user = userId,
patient = patientId,
fileCount = files.Length,
fileNames = files.Select(Path.GetFileName).ToArray()
};
ElasticClient.IndexDocument(logEvent);
}
}
5.2 断点续传可靠性保障
-
分片状态校验:
csharp复制[HttpGet] public ActionResult CheckChunks(string fileMd5, int totalChunks) { var chunkDir = new DirectoryInfo(Path.Combine(Config.TempPath, fileMd5)); if (!chunkDir.Exists) return Json(new { exists = false }); var uploadedChunks = chunkDir.GetFiles() .Select(f => int.Parse(f.Name)) .OrderBy(i => i) .ToArray(); return Json(new { exists = true, uploadedChunks, isComplete = uploadedChunks.Length == totalChunks }); } -
自动修复机制:
javascript复制// 前端自动修复缺失分片 uploader.on('error', function(type) { if (type === 'upload_error') { const failedChunk = this.getChunk(this.currentFile); setTimeout(() => { this.retry(this.currentFile); }, 3000); } });
6. 实际部署建议
6.1 医疗系统环境配置
-
IIS优化参数:
xml复制<!-- applicationHost.config --> <system.applicationHost> <webLimits connectionTimeout="00:10:00" maxConnections="10000" maxBandwidth="4294967295" /> </system.applicationHost> -
数据库索引优化:
sql复制-- 上传记录表索引 CREATE NONCLUSTERED INDEX [IX_UploadLogs_PatientID] ON [dbo].[UploadLogs] ([PatientID]) INCLUDE ([UploadTime], [FileCount], [TotalSize])
6.2 高可用架构设计
code复制医疗上传系统高可用架构:
1. 前端负载均衡:Nginx反向代理多台Web服务器
2. 分片存储集群:分布式文件系统(如FastDFS)存储临时分片
3. 数据库集群:SQL Server AlwaysOn实现数据库高可用
4. 灾备方案:阿里云OSS跨区域复制实现异地容灾
7. 测试验证方案
7.1 医疗数据完整性校验
csharp复制// DICOM文件校验
public bool ValidateDicomFile(string filePath)
{
try {
var dicomFile = DicomFile.Open(filePath);
return dicomFile.Dataset.Contains(DicomTag.PatientID)
&& dicomFile.Dataset.Contains(DicomTag.StudyInstanceUID);
} catch {
return false;
}
}
// 批量校验
public ActionResult VerifyUpload(string studyUid)
{
var files = Directory.GetFiles(Path.Combine(Config.DicomRoot, studyUid),
"*.*", SearchOption.AllDirectories);
var invalidFiles = files
.Where(f => !ValidateDicomFile(f))
.ToArray();
return Json(new {
totalCount = files.Length,
invalidCount = invalidFiles.Length,
invalidFiles
});
}
7.2 兼容性测试矩阵
| 测试项 | Chrome | Firefox | Edge | IE11 | IE8 |
|---|---|---|---|---|---|
| 文件夹上传 | ✓ | ✓ | ✓ | ✓ | ✗ |
| 大文件分片上传 | ✓ | ✓ | ✓ | ✓ | ✓* |
| 断点续传 | ✓ | ✓ | ✓ | ✓ | ✓* |
| 加密传输 | ✓ | ✓ | ✓ | ✓ | ✓* |
注:IE8需依赖Flash组件实现部分功能,标*的功能在IE8下有限支持
8. 扩展功能实现
8.1 与PACS系统集成
csharp复制// DICOM文件自动发送到PACS
public void SendToPACS(string studyUid)
{
var files = Directory.GetFiles(Path.Combine(Config.DicomRoot, studyUid),
"*.dcm", SearchOption.AllDirectories);
using (var pacsClient = new DICOMServiceClient(Config.PACSEndpoint))
{
foreach (var file in files)
{
var dicomFile = DicomFile.Open(file);
pacsClient.CStore(dicomFile);
}
}
// 更新状态
using (var db = new MedicalDbContext())
{
var study = db.Studies.FirstOrDefault(s => s.StudyInstanceUID == studyUid);
if (study != null)
{
study.PACSStatus = "Completed";
db.SaveChanges();
}
}
}
8.2 自动生成缩略图
csharp复制// 为医学影像生成预览缩略图
public void GenerateThumbnails(string studyUid)
{
var files = Directory.GetFiles(Path.Combine(Config.DicomRoot, studyUid),
"*.dcm", SearchOption.AllDirectories);
Parallel.ForEach(files, file => {
using (var image = DicomImage.Open(file))
{
var bitmap = image.RenderImage().AsBitmap();
var thumbnail = new Bitmap(bitmap, new Size(256, 256));
var thumbPath = Path.Combine(
Config.ThumbnailRoot,
studyUid,
Path.GetFileNameWithoutExtension(file) + ".jpg");
Directory.CreateDirectory(Path.GetDirectoryName(thumbPath));
thumbnail.Save(thumbPath, ImageFormat.Jpeg);
}
});
}
9. 实际部署案例
某三甲医院影像上传系统配置:
-
硬件环境:
- Web服务器:Dell R740xd ×4(负载均衡)
- 存储服务器:Dell ME4024 ×2(200TB可用空间)
- 网络:10Gbps光纤内网
-
软件配置:
- OS:Windows Server 2019
- Web服务器:IIS 10.0
- 数据库:SQL Server 2019 AlwaysOn集群
- 缓存:Redis集群
-
性能指标:
- 支持并发上传:500+个检查序列
- 平均上传速度:院内1.2GB/min,院外200MB/min(通过专线)
- 最大单日上传量:12TB(峰值时段)
10. 持续改进方向
-
智能分片策略:
- 基于网络质量检测动态调整分片大小
- 根据文件类型设置不同分片策略(如DICOM文件优先传输文件头)
-
边缘计算预处理:
csharp复制// 在网关节点进行初步校验 public ActionResult EdgePreprocess() { var files = Request.Files; var results = new List<object>(); foreach (HttpPostedFileBase file in files) { try { // 快速校验文件头 var header = new byte[128]; file.InputStream.Read(header, 0, 128); results.Add(new { fileName = file.FileName, isValid = IsValidDicomHeader(header), size = file.ContentLength }); } catch { results.Add(new { fileName = file.FileName, isValid = false, error = "读取失败" }); } } return Json(results); } -
区块链存证:
csharp复制// 上传完成后将文件哈希上链 public void BlockchainNotarization(string filePath) { var hash = ComputeFileHash(filePath); var txHash = _blockchainService.SubmitTransaction( "MedicalUpload", new { FilePath = filePath, Hash = hash, Timestamp = DateTime.UtcNow }); _db.UploadRecords .Where(r => r.FilePath == filePath) .ToList() .ForEach(r => { r.BlockchainTxHash = txHash; }); _db.SaveChanges(); }