namespace AgriculturalPlatform.Api.Services; /// 已保存文件的存储 key 与访问地址 public record StoredFile(string Key, string Url); /// /// 附件文件存储抽象。 /// 当前默认实现为本地存储(wwwroot/uploads); /// 阿里云 OSS 授权验证配置好后(appsettings.json 的 Oss 节点), /// 将 Storage:UseOss 设为 true 即可切换到 OSS。 /// public interface IFileStorage { /// 保存文件,返回存储 key 与访问 URL Task SaveAsync(string subDir, Stream stream, string fileName, string contentType); /// 按 key 删除文件(本地/OSS 通用) Task DeleteAsync(string key); } /// 本地磁盘存储(wwwroot/uploads),URL 即相对路径,由静态文件中间件直接访问 public sealed class LocalFileStorage(IWebHostEnvironment env) : IFileStorage { private readonly string _root = Path.Combine( env.WebRootPath ?? Path.Combine(env.ContentRootPath, "wwwroot"), "uploads"); public async Task SaveAsync(string subDir, Stream stream, string fileName, string contentType) { var dateDir = DateTime.Now.ToString("yyyyMM"); var dir = Path.Combine(_root, subDir, dateDir); Directory.CreateDirectory(dir); var ext = Path.GetExtension(fileName); if (string.IsNullOrWhiteSpace(ext)) ext = ".jpg"; if (ext.Length > 10) ext = ".jpg"; var name = $"{Guid.NewGuid():N}{ext}"; var full = Path.Combine(dir, name); await using var fs = File.Create(full); await stream.CopyToAsync(fs); var key = $"/uploads/{subDir}/{dateDir}/{name}"; return new StoredFile(key, key); } public Task DeleteAsync(string key) { if (string.IsNullOrWhiteSpace(key) || !key.StartsWith("/uploads/")) return Task.CompletedTask; try { var full = Path.Combine(_root, key.TrimStart('/').Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(full)) File.Delete(full); } catch { // 忽略删除失败 } return Task.CompletedTask; } } /// /// 阿里云 OSS 存储。授权验证(AccessKeyId/Secret、Endpoint、Bucket)后期配置提供; /// 未配置前调用会抛出 InvalidOperationException,由接口统一转为友好提示。 /// public sealed class OssFileStorage(IConfiguration cfg) : IFileStorage { public bool IsConfigured => !string.IsNullOrWhiteSpace(cfg["Oss:AccessKeyId"]) && !string.IsNullOrWhiteSpace(cfg["Oss:AccessKeySecret"]) && !string.IsNullOrWhiteSpace(cfg["Oss:Bucket"]); public Task SaveAsync(string subDir, Stream stream, string fileName, string contentType) => throw new InvalidOperationException( "阿里云 OSS 尚未配置授权验证,请在 appsettings.json 的 Oss 节点填写 AccessKeyId / AccessKeySecret / Endpoint / Bucket 后重启服务"); public Task DeleteAsync(string key) => throw new InvalidOperationException("阿里云 OSS 尚未配置授权验证,暂不支持删除"); }