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 尚未配置授权验证,暂不支持删除");
}