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