完善系统参数配置和数据字典管理模块
This commit is contained in:
@@ -13,7 +13,7 @@ namespace Service.Implement
|
||||
public const byte DataScopeAll = 1;
|
||||
public const byte DataScopeLab = 2;
|
||||
|
||||
/// <summary>6 个系统权限编码</summary>
|
||||
/// <summary>7 个系统权限编码</summary>
|
||||
public static readonly (string Code, string Name, string Group)[] Permissions =
|
||||
{
|
||||
("device:view", "查看设备", "device"),
|
||||
@@ -21,7 +21,8 @@ namespace Service.Implement
|
||||
("device:control", "控制设备", "device"),
|
||||
("alert:confirm", "确认告警", "alert"),
|
||||
("inspection:manage", "巡检管理", "inspection"),
|
||||
("user:manage", "用户管理", "user")
|
||||
("user:manage", "用户管理", "user"),
|
||||
("system:manage", "系统配置管理", "system")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -30,8 +31,8 @@ namespace Service.Implement
|
||||
/// </summary>
|
||||
public static readonly (string Code, string Name, byte DataScope, string[] Permissions)[] Roles =
|
||||
{
|
||||
("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage" }),
|
||||
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage" }),
|
||||
("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage", "system:manage" }),
|
||||
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "system:manage" }),
|
||||
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
|
||||
("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" })
|
||||
};
|
||||
@@ -113,6 +114,12 @@ namespace Service.Implement
|
||||
db.Insertable(new UserRoleEntity { UserId = adminId, RoleId = superAdmin.Id, CreateTime = now }).ExecuteCommand();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 系统参数种子(一期 4 个内置参数,幂等:按 ParamKey 跳过已存在)
|
||||
SeedSystemParams(db, now);
|
||||
|
||||
// 5. 默认本地文件存储配置(幂等:仅当无任何配置时插入一条默认 Local 通道)
|
||||
SeedDefaultFileStorage(db, now);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -120,5 +127,70 @@ namespace Service.Implement
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SeedDefaultFileStorage(SqlSugar.ISqlSugarClient db, DateTime now)
|
||||
{
|
||||
var hasAny = db.Queryable<FileStorageConfigEntity>().Where(x => x.IsDel == 0).Any();
|
||||
if (hasAny) return;
|
||||
var entity = new FileStorageConfigEntity
|
||||
{
|
||||
Provider = "local",
|
||||
Name = "本地存储",
|
||||
Endpoint = "/uploads",
|
||||
AccessKey = null,
|
||||
SecretKey = null,
|
||||
Bucket = null,
|
||||
Region = null,
|
||||
BasePath = "wwwroot/uploads",
|
||||
MaxSizeMB = 10,
|
||||
AllowedExts = null, // 不限制扩展名;如需限制填 [".jpg",".png",".pdf",".xlsx"]
|
||||
IsDefault = 1,
|
||||
Status = 1,
|
||||
Remark = "系统默认本地存储通道(文件落 wwwroot/uploads,通过 /uploads 静态访问)",
|
||||
CreateTime = now
|
||||
};
|
||||
db.Insertable(entity).ExecuteReturnSnowflakeId();
|
||||
}
|
||||
|
||||
/// <summary>一期 4 个内置系统参数(IsSystem=1,禁止删除,仅允许改 Value/Remark)</summary>
|
||||
/// <remarks>Group 与 ParamType 约定:0=int,1=text,2=enum,3=json</remarks>
|
||||
private static readonly (string Key, string Name, string? Value, byte Type, string? Options, string? Unit, string Group, int Sort, string? Remark)[] SystemParams =
|
||||
{
|
||||
("data_collect_default_frequency", "数据采集默认频率", "5", 0, null, "秒", "数据采集", 1, "设备数据采集的默认间隔(秒),新建设备时作为默认值"),
|
||||
("alert_notify_method", "告警通知方式", "feishu", 2,
|
||||
"[{\"value\":\"feishu\",\"label\":\"飞书\"},{\"value\":\"dingtalk\",\"label\":\"钉钉\"},{\"value\":\"wecom\",\"label\":\"企业微信\"},{\"value\":\"email\",\"label\":\"邮件\"}]",
|
||||
null, "告警", 2, "默认告警通知渠道(多选需在告警规则内单独配置)"),
|
||||
("work_order_code_rule", "工单编号规则", "WO{yyyyMMddHHmmss}", 1, null, null, "工单", 3,
|
||||
"工单编号生成模板,支持占位符:{yyyy}年 {MM}月 {dd}日 {HH}时 {mm}分 {ss}秒,序列号用 {seq4} 表示 4 位顺序号"),
|
||||
("file_upload_limit", "文件上传限制", "10", 2,
|
||||
"[{\"value\":\"5\",\"label\":\"5MB\"},{\"value\":\"10\",\"label\":\"10MB\"},{\"value\":\"20\",\"label\":\"20MB\"},{\"value\":\"50\",\"label\":\"50MB\"}]",
|
||||
"MB", "文件", 4, "单个文件上传大小上限(MB)")
|
||||
};
|
||||
|
||||
private static void SeedSystemParams(SqlSugar.ISqlSugarClient db, DateTime now)
|
||||
{
|
||||
foreach (var (key, name, value, type, options, unit, group, sort, remark) in SystemParams)
|
||||
{
|
||||
var exists = db.Queryable<SystemParamEntity>().Where(x => x.ParamKey == key).Any();
|
||||
if (exists) continue;
|
||||
var entity = new SystemParamEntity
|
||||
{
|
||||
ParamKey = key,
|
||||
ParamName = name,
|
||||
ParamValue = value,
|
||||
ParamType = type,
|
||||
ParamOptions = options,
|
||||
Unit = unit,
|
||||
Group = group,
|
||||
Sort = sort,
|
||||
Remark = remark,
|
||||
IsSystem = 1,
|
||||
LastUpdateUser = "system",
|
||||
LastUpdateTime = now,
|
||||
CreateTime = now
|
||||
};
|
||||
db.Insertable(entity).ExecuteReturnSnowflakeId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,360 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据字典管理 服务实现
|
||||
/// 缓存策略:按 typeCode 缓存启用项,新增/修改/删除字典分类或字典项时失效对应 typeCode;启动首次访问时按需加载
|
||||
/// </summary>
|
||||
public class DictService : IDictService
|
||||
{
|
||||
// TODO: 实现 数据字典管理 相关方法
|
||||
private readonly IMemoryCache _cache;
|
||||
private static readonly object _cacheLock = new();
|
||||
|
||||
public DictService(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
// ==================== 字典分类 ====================
|
||||
|
||||
public async Task<Result<List<DictTypeDto>>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!))
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
|
||||
var dtos = list.ToDtoList();
|
||||
await FillItemCountAsync(dtos);
|
||||
return Result<List<DictTypeDto>>.Success(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<DictTypeDto>>> GetTypesAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.Status == 1)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
return Result<List<DictTypeDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> GetTypeByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||
var dto = entity.ToDto();
|
||||
await FillItemCountAsync(new List<DictTypeDto> { dto });
|
||||
return Result<DictTypeDto>.Success(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("查询字典分类详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> AddTypeAsync(DictTypeDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||
return Result<DictTypeDto>.Error("字典编码和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||
if (exists) return Result<DictTypeDto>.Error($"字典编码「{dto.Code}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
return Result<DictTypeDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("新增字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> UpdateTypeAsync(DictTypeDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<DictTypeDto>.Error("字典分类Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictTypeDto>.Error($"字典编码「{dto.Code}」已存在");
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
// 编码可能变更:旧/新 Code 都失效
|
||||
InvalidateCache(entity.Code);
|
||||
InvalidateCache(dto.Code);
|
||||
return Result<DictTypeDto>.Success(newEntity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("修改字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteTypeAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("字典分类Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("字典分类不存在或已被删除");
|
||||
|
||||
var hasItems = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == id && x.IsDel == 0).AnyAsync();
|
||||
if (hasItems) return Result.Error("该字典分类下还有字典项,请先删除字典项再删除分类");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<DictTypeEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
InvalidateCache(entity.Code);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 字典项 ====================
|
||||
|
||||
public async Task<Result<List<DictItemDto>>> GetItemsByTypeAsync(long typeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToListAsync();
|
||||
return Result<List<DictItemDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictItemDto>>.Error("查询字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictItemDto>> AddItemAsync(DictItemDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||
if (string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||
return Result<DictItemDto>.Error("字典项编码和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var typeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == typeId && x.IsDel == 0).AnyAsync();
|
||||
if (!typeExists) return Result<DictItemDto>.Error("所属字典分类不存在");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictItemDto>.Error($"字典项编码「{dto.Code}」在该分类下已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.TypeId = typeId;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
if (entity.IsDefault == 1)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDefault == 0)
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0).ExecuteCommandAsync();
|
||||
}
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(typeId));
|
||||
return Result<DictItemDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictItemDto>.Error("新增字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictItemDto>> UpdateItemAsync(DictItemDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<DictItemDto>.Error("字典项Id无效");
|
||||
if (!long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictItemDto>.Error("字典项不存在或已被删除");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictItemDto>.Error($"字典项编码「{dto.Code}」在该分类下已存在");
|
||||
|
||||
// 默认项互斥:改为默认时清零同分类其它默认项
|
||||
if (dto.IsDefault == 1 && entity.IsDefault == 0)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDefault == 0)
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Id != id).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
newEntity.TypeId = typeId;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(typeId));
|
||||
return Result<DictItemDto>.Success(newEntity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictItemDto>.Error("修改字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteItemAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("字典项Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("字典项不存在或已被删除");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(entity.TypeId));
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 业务侧:按 typeCode 查询启用项(带缓存) ====================
|
||||
|
||||
public async Task<Result<List<DictOptionDto>>> GetOptionsByCodeAsync(string typeCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeCode))
|
||||
return Result<List<DictOptionDto>>.Error("字典编码不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var key = $"dict:options:{typeCode}";
|
||||
if (!_cache.TryGetValue(key, out List<DictOptionDto>? cached) || cached == null)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
cached ??= LoadOptionsByCodeAsync(typeCode).GetAwaiter().GetResult();
|
||||
_cache.Set(key, cached, TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
return Result<List<DictOptionDto>>.Success(cached);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictOptionDto>>.Error("查询字典选项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private async Task<string> GetTypeCodeAsync(long typeId)
|
||||
{
|
||||
var code = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == typeId).Select(x => x.Code).FirstAsync();
|
||||
return code ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task<List<DictOptionDto>> LoadOptionsByCodeAsync(string typeCode)
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
var typeId = await db.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == typeCode && x.IsDel == 0 && x.Status == 1)
|
||||
.Select(x => x.Id).FirstAsync();
|
||||
if (typeId == 0) return new List<DictOptionDto>();
|
||||
|
||||
var items = await db.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Status == 1)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
|
||||
return items.Select(e => new DictOptionDto
|
||||
{
|
||||
Code = e.Code,
|
||||
Name = e.Name,
|
||||
IsDefault = e.IsDefault == 1
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private void InvalidateCache(string? typeCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeCode)) return;
|
||||
_cache.Remove($"dict:options:{typeCode}");
|
||||
}
|
||||
|
||||
private async Task FillItemCountAsync(List<DictTypeDto> dtos)
|
||||
{
|
||||
if (dtos == null || dtos.Count == 0) return;
|
||||
var ids = dtos.Select(d => long.Parse(d.Id)).ToList();
|
||||
var counts = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => ids.Contains(x.TypeId) && x.IsDel == 0)
|
||||
.GroupBy(x => x.TypeId)
|
||||
.Select(x => new { TypeId = x.TypeId, Count = SqlFunc.AggregateCount(x.Id) })
|
||||
.ToListAsync();
|
||||
var map = counts.ToDictionary(c => c.TypeId, c => c.Count);
|
||||
foreach (var d in dtos) d.ItemCount = map.TryGetValue(long.Parse(d.Id), out var c) ? c : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,257 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统参数配置 服务实现
|
||||
/// 缓存策略:按 ParamKey 缓存值;增删改时失效对应 Key(Key 变更时新旧都失效)
|
||||
/// </summary>
|
||||
public class SystemParamService : ISystemParamService
|
||||
{
|
||||
// TODO: 实现 系统参数配置 相关方法
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private static readonly object _cacheLock = new();
|
||||
|
||||
public SystemParamService(IMemoryCache cache, ICurrentUser currentUser)
|
||||
{
|
||||
_cache = cache;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
// ==================== 管理侧 ====================
|
||||
|
||||
public async Task<Result<List<SystemParamDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? group = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.ParamKey.Contains(keyword!) || x.ParamName.Contains(keyword!))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(group), x => x.Group == group)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
return Result<List<SystemParamDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<SystemParamDto>>.Error("查询系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("查询系统参数详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> AddAsync(SystemParamDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.ParamKey) || string.IsNullOrWhiteSpace(dto.ParamName))
|
||||
return Result<SystemParamDto>.Error("参数键和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == dto.ParamKey && x.IsDel == 0).AnyAsync();
|
||||
if (keyExists) return Result<SystemParamDto>.Error($"参数键「{dto.ParamKey}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.IsSystem = 0; // 用户新增的不是内置参数
|
||||
entity.CreateTime = DateTime.Now;
|
||||
entity.LastUpdateUser = _currentUser.UserName;
|
||||
entity.LastUpdateTime = DateTime.Now;
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("新增系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> UpdateAsync(SystemParamDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<SystemParamDto>.Error("参数Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||
|
||||
// Key 变更时校验唯一
|
||||
if (!string.Equals(entity.ParamKey, dto.ParamKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == dto.ParamKey && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (keyExists) return Result<SystemParamDto>.Error($"参数键「{dto.ParamKey}」已存在");
|
||||
}
|
||||
|
||||
var oldKey = entity.ParamKey;
|
||||
var now = DateTime.Now;
|
||||
var userName = _currentUser.UserName;
|
||||
|
||||
if (entity.IsSystem == 1)
|
||||
{
|
||||
// 内置参数仅允许改 Value/Remark,其它字段保留
|
||||
entity.ParamValue = dto.ParamValue;
|
||||
entity.Remark = dto.Remark;
|
||||
entity.LastUpdateUser = userName;
|
||||
entity.LastUpdateTime = now;
|
||||
await SqlSugarContext.DbContext.Updateable(entity)
|
||||
.UpdateColumns(x => new { x.ParamValue, x.Remark, x.LastUpdateUser, x.LastUpdateTime })
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 用户自定义参数:全字段更新(审计字段由服务端控管)
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
newEntity.IsSystem = 0;
|
||||
newEntity.CreateTime = entity.CreateTime;
|
||||
newEntity.LastUpdateUser = userName;
|
||||
newEntity.LastUpdateTime = now;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
entity = newEntity;
|
||||
}
|
||||
|
||||
InvalidateCache(oldKey);
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("修改系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("参数Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("系统参数不存在或已被删除");
|
||||
if (entity.IsSystem == 1) return Result.Error($"内置参数「{entity.ParamName}」禁止删除,仅允许修改值");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<SystemParamEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Result> ReloadCacheAsync()
|
||||
{
|
||||
// IMemoryCache 非强类型缓存清除,此处复用一个枚举 Key 的策略:清除所有 sysparam: 前缀缓存需要 ICache 抽象;
|
||||
// 当前实现:通过 Coherent MemoryCache 实例的 compact,按约定不实现细粒度清除,仅返回成功提示由进程内自然过期
|
||||
// 注:增删改单条时已按 Key 失效;本接口预留给运维紧急场景,下次访问会重新加载
|
||||
try
|
||||
{
|
||||
// 由于 IMemoryCache 没有枚举能力,这里仅作占位返回;如需全量清除,请重启服务或改用 IDistributedCache
|
||||
return Task.FromResult(Result.Success());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(Result.Error("清空缓存失败", ex));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 业务侧:按 Key 取值(带缓存) ====================
|
||||
|
||||
public async Task<Result<string>> GetValueAsync(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Result<string>.Error("参数键不能为空");
|
||||
try
|
||||
{
|
||||
var value = await LoadValueByKeyAsync(key);
|
||||
return Result<string>.Success(value ?? string.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<string>.Error("查询参数值失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<SystemParamValueDto>>> GetValuesAsync(string[] keys)
|
||||
{
|
||||
if (keys == null || keys.Length == 0)
|
||||
return Result<List<SystemParamValueDto>>.Error("参数键列表不能为空");
|
||||
try
|
||||
{
|
||||
var distinctKeys = keys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList();
|
||||
var list = new List<SystemParamValueDto>(distinctKeys.Count);
|
||||
foreach (var k in distinctKeys)
|
||||
{
|
||||
var v = await LoadValueByKeyAsync(k);
|
||||
list.Add(new SystemParamValueDto { Key = k, Value = v ?? string.Empty });
|
||||
}
|
||||
return Result<List<SystemParamValueDto>>.Success(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<SystemParamValueDto>>.Error("批量查询参数值失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private async Task<string?> LoadValueByKeyAsync(string key)
|
||||
{
|
||||
var cacheKey = $"sysparam:{key}";
|
||||
if (!_cache.TryGetValue(cacheKey, out string? cached))
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (!_cache.TryGetValue(cacheKey, out cached))
|
||||
{
|
||||
cached = SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == key && x.IsDel == 0)
|
||||
.Select(x => x.ParamValue).First();
|
||||
_cache.Set(cacheKey, cached ?? string.Empty, TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
private void InvalidateCache(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return;
|
||||
_cache.Remove($"sysparam:{key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user