Files

323 lines
15 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 文件存储管理 服务实现
/// 通过 IEnumerable&lt;IFileStorageProvider&gt; 按配置 Provider 字段挑选实现;当前仅 Local 落地
/// </summary>
public class FileStorageService : IFileStorageService
{
private readonly IEnumerable<IFileStorageProvider> _providers;
private readonly ICurrentUser _currentUser;
// 掩码标记:前端回显 AccessKey/SecretKey 时用 **** 脱敏,回写时若仍为掩码则保留原值
private const string MaskMarker = "****";
public FileStorageService(IEnumerable<IFileStorageProvider> providers, ICurrentUser currentUser)
{
_providers = providers;
_currentUser = currentUser;
}
// ==================== 存储配置 ====================
public async Task<Result<List<FileStorageConfigDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Name.Contains(keyword!) || x.Provider.Contains(keyword!))
.OrderBy(x => x.IsDefault, OrderByType.Desc).OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
var dtos = list.ToDtoList();
// AccessKey/SecretKey 脱敏回显
foreach (var d in dtos) { d.AccessKey = Mask(d.AccessKey); d.SecretKey = Mask(d.SecretKey); }
return Result<List<FileStorageConfigDto>>.Success(dtos);
}
catch (Exception ex)
{
return Result<List<FileStorageConfigDto>>.Error("查询存储配置失败", ex);
}
}
public async Task<Result<FileStorageConfigDto>> GetByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<FileStorageConfigDto>.Error("存储配置不存在或已被删除");
var dto = entity.ToDto();
dto.AccessKey = Mask(dto.AccessKey);
dto.SecretKey = Mask(dto.SecretKey);
return Result<FileStorageConfigDto>.Success(dto);
}
catch (Exception ex)
{
return Result<FileStorageConfigDto>.Error("查询存储配置失败", ex);
}
}
public async Task<Result<FileStorageConfigDto>> AddAsync(FileStorageConfigDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.Provider) || string.IsNullOrWhiteSpace(dto.Name))
return Result<FileStorageConfigDto>.Error("Provider 和名称不能为空");
if (!IsProviderSupported(dto.Provider))
return Result<FileStorageConfigDto>.Error($"不支持的 Provider{dto.Provider}(当前仅支持 local");
try
{
var entity = dto.ToEntity();
entity.Id = 0;
entity.CreateTime = DateTime.Now;
if (entity.IsDefault == 1) await ClearOtherDefaultAsync(0);
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
var result = entity.ToDto();
result.AccessKey = Mask(result.AccessKey);
result.SecretKey = Mask(result.SecretKey);
return Result<FileStorageConfigDto>.Success(result);
}
catch (Exception ex)
{
return Result<FileStorageConfigDto>.Error("新增存储配置失败", ex);
}
}
public async Task<Result<FileStorageConfigDto>> UpdateAsync(FileStorageConfigDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<FileStorageConfigDto>.Error("配置Id无效");
if (!string.IsNullOrWhiteSpace(dto.Provider) && !IsProviderSupported(dto.Provider))
return Result<FileStorageConfigDto>.Error($"不支持的 Provider{dto.Provider}");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<FileStorageConfigDto>.Error("存储配置不存在或已被删除");
// AccessKey/SecretKey 为掩码时保留原值(前端未改密钥)
var newEntity = dto.ToEntity();
newEntity.Id = id;
newEntity.CreateTime = entity.CreateTime;
if (string.IsNullOrEmpty(newEntity.AccessKey) || newEntity.AccessKey.Contains(MaskMarker))
newEntity.AccessKey = entity.AccessKey;
if (string.IsNullOrEmpty(newEntity.SecretKey) || newEntity.SecretKey.Contains(MaskMarker))
newEntity.SecretKey = entity.SecretKey;
if (newEntity.IsDefault == 1) await ClearOtherDefaultAsync(id);
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
.ExecuteCommandAsync();
var result = newEntity.ToDto();
result.AccessKey = Mask(result.AccessKey);
result.SecretKey = Mask(result.SecretKey);
return Result<FileStorageConfigDto>.Success(result);
}
catch (Exception ex)
{
return Result<FileStorageConfigDto>.Error("修改存储配置失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("配置Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("存储配置不存在或已被删除");
if (entity.IsDefault == 1) return Result.Error($"配置「{entity.Name}」是默认通道,禁止删除(请先切换默认)");
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除存储配置失败", ex);
}
}
public async Task<Result> SetDefaultAsync(long id)
{
if (id <= 0) return Result.Error("配置Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("存储配置不存在或已被删除");
if (entity.Status == 0) return Result.Error("已停用的配置不能设为默认");
await ClearOtherDefaultAsync(id);
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
.SetColumns(x => x.IsDefault == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("设置默认存储失败", ex);
}
}
public async Task<Result> TestAsync(long id)
{
if (id <= 0) return Result.Error("配置Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("存储配置不存在或已被删除");
var provider = ResolveProvider(entity.Provider);
if (provider == null) return Result.Error($"未注册 Provider{entity.Provider}");
return await provider.TestConnectionAsync(entity);
}
catch (Exception ex)
{
return Result.Error("测试连接失败", ex);
}
}
// ==================== 文件上传/记录 ====================
public async Task<Result<FileRecordDto>> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null)
{
if (stream == null || size <= 0) return Result<FileRecordDto>.Error("文件流或大小无效");
if (string.IsNullOrWhiteSpace(originalName)) return Result<FileRecordDto>.Error("文件名不能为空");
try
{
// 取默认启用的存储配置
var config = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
.Where(x => x.IsDel == 0 && x.IsDefault == 1 && x.Status == 1).FirstAsync();
if (config == null) return Result<FileRecordDto>.Error("未配置默认存储通道,请先在「文件存储管理」新增并设为默认");
// 大小校验
if (config.MaxSizeMB > 0 && size > config.MaxSizeMB * 1024L * 1024L)
return Result<FileRecordDto>.Error($"文件大小超过上限 {config.MaxSizeMB}MB");
// 扩展名校验
var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? "";
var allowed = ParseExts(config.AllowedExts);
if (allowed.Count > 0 && !allowed.Contains(ext))
return Result<FileRecordDto>.Error($"不支持的文件类型 {ext}(允许:{string.Join(",", allowed)}");
var provider = ResolveProvider(config.Provider);
if (provider == null) return Result<FileRecordDto>.Error($"未注册 Provider{config.Provider}");
var upRes = await provider.UploadAsync(stream, originalName, size, config);
if (!upRes.IsSuccess || upRes.Data == null) return Result<FileRecordDto>.Error(upRes.Msg);
var record = new FileRecordEntity
{
FileName = upRes.Data.FileName,
OriginalName = originalName,
Url = upRes.Data.Url,
Size = size,
Ext = ext,
Provider = config.Provider,
StorageId = config.Id,
Uploader = string.IsNullOrWhiteSpace(uploader) ? _currentUser.UserName : uploader,
BizType = string.IsNullOrWhiteSpace(bizType) ? "generic" : bizType,
BizId = bizId,
CreateTime = DateTime.Now
};
var rid = await SqlSugarContext.DbContext.Insertable(record).ExecuteReturnSnowflakeIdAsync();
record.Id = rid;
return Result<FileRecordDto>.Success(record.ToDto());
}
catch (Exception ex)
{
return Result<FileRecordDto>.Error("文件上传失败", ex);
}
}
public async Task<Result<List<FileRecordDto>>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? bizType = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.FileName.Contains(keyword!) || x.OriginalName!.Contains(keyword!))
.WhereIF(!string.IsNullOrWhiteSpace(bizType), x => x.BizType == bizType)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
return Result<List<FileRecordDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<FileRecordDto>>.Error("查询文件记录失败", ex);
}
}
public async Task<Result<FileRecordDto>> GetFileByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<FileRecordDto>.Error("文件记录不存在或已被删除");
return Result<FileRecordDto>.Success(entity.ToDto());
}
catch (Exception ex)
{
return Result<FileRecordDto>.Error("查询文件记录失败", ex);
}
}
// ==================== 私有工具 ====================
private IFileStorageProvider? ResolveProvider(string? providerName)
{
if (string.IsNullOrWhiteSpace(providerName)) return null;
return _providers.FirstOrDefault(p => p.ProviderName == providerName);
}
private static bool IsProviderSupported(string provider)
=> provider == "local" || provider == "minio" || provider == "oss";
private static string? Mask(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return value;
if (value.Length <= 4) return MaskMarker;
return value[..2] + MaskMarker + value[^2..];
}
private static List<string> ParseExts(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return new List<string>();
try
{
var arr = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
return arr?.Select(e => e.ToLowerInvariant()).ToList() ?? new List<string>();
}
catch { return new List<string>(); }
}
private async Task ClearOtherDefaultAsync(long keepId)
{
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
.SetColumns(x => x.IsDefault == 0)
.Where(x => x.IsDel == 0 && x.Id != keepId).ExecuteCommandAsync();
}
}
}