diff --git a/src/Oms.Application/Services/Storage/FileStorageService.cs b/src/Oms.Application/Services/Storage/FileStorageService.cs
index f2d91b0..016b3b2 100644
--- a/src/Oms.Application/Services/Storage/FileStorageService.cs
+++ b/src/Oms.Application/Services/Storage/FileStorageService.cs
@@ -108,6 +108,55 @@ public class FileStorageService : ApplicationService
var key = FileStorage.ToStorageKey(input.PathOrUrl);
return new NormalizeKeyOutput { Key = key };
}
+
+ ///
+ /// 按 key / URL 流式读取文件(供面单预览等场景,避免前端跨域 iframe 空白)。
+ ///
+ [HttpGet("/file-storage/stream")]
+ public async Task StreamAsync([FromQuery] string key, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new BusinessException("key 不能为空");
+ }
+
+ string storageKey;
+ try
+ {
+ storageKey = FileStorage.ToStorageKey(key) ?? FileStoragePath.NormalizeKey(key);
+ }
+ catch (Exception)
+ {
+ throw new BusinessException("无法解析文件 key");
+ }
+
+ if (!await FileStorage.ExistsAsync(storageKey, cancellationToken))
+ {
+ throw new BusinessException("文件不存在或已过期");
+ }
+
+ var stream = await FileStorage.OpenReadAsync(storageKey, cancellationToken);
+ return new FileStreamResult(stream, GuessStreamContentType(storageKey))
+ {
+ EnableRangeProcessing = true,
+ };
+ }
+
+ private static string GuessStreamContentType(string storageKey)
+ {
+ if (storageKey.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/pdf";
+ }
+
+ if (storageKey.EndsWith(".html", StringComparison.OrdinalIgnoreCase)
+ || storageKey.EndsWith(".htm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/html; charset=utf-8";
+ }
+
+ return "application/octet-stream";
+ }
}
public class PresignUploadInput