完善系统参数配置和数据字典管理模块
This commit is contained in:
@@ -1,12 +1,322 @@
|
||||
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<IFileStorageProvider> 按配置 Provider 字段挑选实现;当前仅 Local 落地
|
||||
/// </summary>
|
||||
public class FileStorageService : IFileStorageService
|
||||
{
|
||||
// TODO: 实现 文件存储管理 相关方法
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user