Bladeren bron

feat: 🙂重写配置参数和字典租户隔离模式

喵你个旺呀 1 jaar geleden
bovenliggende
commit
13b0facd20

+ 10 - 11
Admin.NET/Admin.NET.Core/Entity/SysConfig.cs

@@ -1,4 +1,4 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
 //
 // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
 //
@@ -7,13 +7,13 @@
 namespace Admin.NET.Core;
 
 /// <summary>
-/// 系统参数配置
+/// 系统配置参数表
 /// </summary>
-[SugarTable(null, "系统参数配置表")]
+[SugarTable(null, "系统配置参数表")]
 [SysTable]
 [SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)]
 [SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)]
-public partial class SysConfig : EntityTenant
+public partial class SysConfig : EntityBase
 {
     /// <summary>
     /// 名称
@@ -30,18 +30,17 @@ public partial class SysConfig : EntityTenant
     public string? Code { get; set; }
 
     /// <summary>
-    /// 属性
+    /// 参数
     /// </summary>
-    [SugarColumn(ColumnDescription = "属性值", Length = -1)]
-    [MaxLength(64)]
-    [IgnoreUpdateSeedColumn]
+    [SugarColumn(ColumnDescription = "参数值", Length = 512)]
+    [MaxLength(512)]
     public string? Value { get; set; }
 
     /// <summary>
     /// 是否是内置参数(Y-是,N-否)
     /// </summary>
-    [SugarColumn(ColumnDescription = "是否是内置参数")]
-    public YesNoEnum SysFlag { get; set; }
+    [SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")]
+    public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y;
 
     /// <summary>
     /// 分组编码
@@ -53,7 +52,7 @@ public partial class SysConfig : EntityTenant
     /// <summary>
     /// 排序
     /// </summary>
-    [SugarColumn(ColumnDescription = "排序")]
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
     public int OrderNo { get; set; } = 100;
 
     /// <summary>

+ 64 - 0
Admin.NET/Admin.NET.Core/Entity/SysConfigTenant.cs

@@ -0,0 +1,64 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统租户配置参数表
+/// </summary>
+[SugarTable(null, "系统租户配置参数表")]
+[SysTable]
+[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)]
+[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)]
+public partial class SysConfigTenant : EntityTenant
+{
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "名称", Length = 64)]
+    [Required, MaxLength(64)]
+    public virtual string Name { get; set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "编码", Length = 64)]
+    [MaxLength(64)]
+    public string? Code { get; set; }
+
+    /// <summary>
+    /// 参数值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "参数值", Length = 512)]
+    [MaxLength(512)]
+    public string? Value { get; set; }
+
+    /// <summary>
+    /// 是否是内置参数(Y-是,N-否)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")]
+    public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y;
+
+    /// <summary>
+    /// 分组编码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "分组编码", Length = 64)]
+    [MaxLength(64)]
+    public string? GroupCode { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
+    public int OrderNo { get; set; } = 100;
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注", Length = 256)]
+    [MaxLength(256)]
+    public string? Remark { get; set; }
+}

+ 19 - 12
Admin.NET/Admin.NET.Core/Entity/SysDictData.cs

@@ -11,8 +11,8 @@ namespace Admin.NET.Core;
 /// </summary>
 [SugarTable(null, "系统字典值表")]
 [SysTable]
-[SugarIndex("index_{table}_C", nameof(Value), OrderByType.Asc)]
-public partial class SysDictData : EntityTenant
+[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)]
+public partial class SysDictData : EntityBase
 {
     /// <summary>
     /// 字典类型Id
@@ -20,6 +20,14 @@ public partial class SysDictData : EntityTenant
     [SugarColumn(ColumnDescription = "字典类型Id")]
     public long DictTypeId { get; set; }
 
+    /// <summary>
+    /// 字典类型
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    [Navigate(NavigateType.OneToOne, nameof(DictTypeId))]
+    public SysDictType DictType { get; set; }
+
     /// <summary>
     /// 显示文本
     /// </summary>
@@ -42,6 +50,13 @@ public partial class SysDictData : EntityTenant
     [SugarColumn(ColumnDescription = "编码", Length = 256)]
     public virtual string? Code { get; set; }
 
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "名称", Length = 256)]
+    [MaxLength(256)]
+    public virtual string? Name { get; set; }
+
     /// <summary>
     /// 显示样式-标签颜色
     /// </summary>
@@ -66,7 +81,7 @@ public partial class SysDictData : EntityTenant
     /// <summary>
     /// 排序
     /// </summary>
-    [SugarColumn(ColumnDescription = "排序")]
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
     public int OrderNo { get; set; } = 100;
 
     /// <summary>
@@ -85,14 +100,6 @@ public partial class SysDictData : EntityTenant
     /// <summary>
     /// 状态
     /// </summary>
-    [SugarColumn(ColumnDescription = "状态")]
+    [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
     public StatusEnum Status { get; set; } = StatusEnum.Enable;
-
-    /// <summary>
-    /// 字典类型
-    /// </summary>
-    [Newtonsoft.Json.JsonIgnore]
-    [System.Text.Json.Serialization.JsonIgnore]
-    [Navigate(NavigateType.OneToOne, nameof(DictTypeId))]
-    public SysDictType DictType { get; set; }
 }

+ 105 - 0
Admin.NET/Admin.NET.Core/Entity/SysDictDataTenant.cs

@@ -0,0 +1,105 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统租户字典值表
+/// </summary>
+[SugarTable(null, "系统租户字典值表")]
+[SysTable]
+[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)]
+public partial class SysDictDataTenant : EntityTenant
+{
+    /// <summary>
+    /// 字典类型Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字典类型Id")]
+    public long DictTypeId { get; set; }
+
+    /// <summary>
+    /// 字典类型
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    [Navigate(NavigateType.OneToOne, nameof(DictTypeId))]
+    public SysDictType DictType { get; set; }
+
+    /// <summary>
+    /// 显示文本
+    /// </summary>
+    [SugarColumn(ColumnDescription = "显示文本", Length = 256)]
+    [Required, MaxLength(256)]
+    public virtual string Label { get; set; }
+
+    /// <summary>
+    /// 值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "值", Length = 256)]
+    [Required, MaxLength(256)]
+    public virtual string Value { get; set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    /// <remarks>
+    /// </remarks>
+    [SugarColumn(ColumnDescription = "编码", Length = 256)]
+    public virtual string? Code { get; set; }
+
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "名称", Length = 256)]
+    [MaxLength(256)]
+    public virtual string? Name { get; set; }
+
+    /// <summary>
+    /// 显示样式-标签颜色
+    /// </summary>
+    [SugarColumn(ColumnDescription = "显示样式-标签颜色", Length = 16)]
+    [MaxLength(16)]
+    public string? TagType { get; set; }
+
+    /// <summary>
+    /// 显示样式-Style(控制显示样式)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "显示样式-Style", Length = 512)]
+    [MaxLength(512)]
+    public string? StyleSetting { get; set; }
+
+    /// <summary>
+    /// 显示样式-Class(控制显示样式)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "显示样式-Class", Length = 512)]
+    [MaxLength(512)]
+    public string? ClassSetting { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
+    public int OrderNo { get; set; } = 100;
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注", Length = 2048)]
+    [MaxLength(2048)]
+    public string? Remark { get; set; }
+
+    /// <summary>
+    /// 拓展数据(保存业务功能的配置项)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "拓展数据(保存业务功能的配置项)", ColumnDataType = StaticConfig.CodeFirst_BigString)]
+    public string? ExtData { get; set; }
+
+    /// <summary>
+    /// 状态
+    /// </summary>
+    [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
+    public StatusEnum Status { get; set; } = StatusEnum.Enable;
+}

+ 13 - 7
Admin.NET/Admin.NET.Core/Entity/SysDictType.cs

@@ -32,7 +32,7 @@ public partial class SysDictType : EntityBase
     /// <summary>
     /// 排序
     /// </summary>
-    [SugarColumn(ColumnDescription = "排序")]
+    [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
     public int OrderNo { get; set; } = 100;
 
     /// <summary>
@@ -43,16 +43,22 @@ public partial class SysDictType : EntityBase
     public string? Remark { get; set; }
 
     /// <summary>
-    /// 系统内置
+    /// 状态
     /// </summary>
-    [SugarColumn(ColumnDescription = "系统内置")]
-    public YesNoEnum SysFlag { get; set; } = YesNoEnum.N;
+    [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
+    public StatusEnum Status { get; set; } = StatusEnum.Enable;
 
     /// <summary>
-    /// 状态
+    /// 是否是内置字典(Y-是,N-否)
     /// </summary>
-    [SugarColumn(ColumnDescription = "状态")]
-    public StatusEnum Status { get; set; } = StatusEnum.Enable;
+    [SugarColumn(ColumnDescription = "是否是内置字典", DefaultValue = "1")]
+    public virtual YesNoEnum SysFlag { get; set; } = YesNoEnum.Y;
+
+    /// <summary>
+    /// 是否是租户字典(Y-是,N-否)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否是租户字典", DefaultValue = "2")]
+    public virtual YesNoEnum IsTenant { get; set; } = YesNoEnum.N;
 
     /// <summary>
     /// 字典值集合

+ 12 - 0
Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs

@@ -385,6 +385,18 @@ public enum ErrorCodeEnum
     [ErrorCodeItemMetadata("非超管用户禁止操作系统字典")]
     D3010,
 
+    /// <summary>
+    /// 获取字典值集合入参有误
+    /// </summary>
+    [ErrorCodeItemMetadata("获取字典值集合入参有误")]
+    D3011,
+
+    /// <summary>
+    /// 禁止修改租户字典状态
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止修改租户字典状态")]
+    D3012,
+
     /// <summary>
     /// 菜单已存在
     /// </summary>

+ 49 - 0
Admin.NET/Admin.NET.Core/Service/Config/Dto/TenantConfigInput.cs

@@ -0,0 +1,49 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+public class ConfigTenantInput : BaseIdInput;
+
+public class PageConfigTenantInput : BasePageInput
+{
+    /// <summary>
+    /// 名称
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    public string Code { get; set; }
+
+    /// <summary>
+    /// 分组编码
+    /// </summary>
+    public string GroupCode { get; set; }
+}
+
+public class AddConfigTenantInput : SysConfigTenant;
+
+public class UpdateConfigTenantInput : AddConfigTenantInput;
+
+public class DeleteConfigTenantInput : BaseIdInput;
+
+/// <summary>
+/// 批量配置参数输入
+/// </summary>
+public class BatchConfigTenantInput
+{
+    /// <summary>
+    /// 编码
+    /// </summary>
+    public string Code { get; set; }
+
+    /// <summary>
+    /// 属性值
+    /// </summary>
+    public string Value { get; set; }
+}

+ 7 - 18
Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs

@@ -40,8 +40,7 @@ public class SysConfigService : IDynamicApiController, ITransient
     [DisplayName("获取参数配置分页列表")]
     public async Task<SqlSugarPagedList<SysConfig>> Page(PageConfigInput input)
     {
-        return await _sysConfigRep.AsQueryable().ClearFilter()
-            .WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId)
+        return await _sysConfigRep.AsQueryable()
             .WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name))
             .WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code))
             .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
@@ -56,7 +55,7 @@ public class SysConfigService : IDynamicApiController, ITransient
     [DisplayName("获取参数配置列表")]
     public async Task<List<SysConfig>> List(PageConfigInput input)
     {
-        return await GetConfigQueryable()
+        return await _sysConfigRep.AsQueryable()
             .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
             .ToListAsync();
     }
@@ -163,7 +162,7 @@ public class SysConfigService : IDynamicApiController, ITransient
         var value = _sysCacheService.Get<string>($"{CacheConst.KeyConfig}{code}");
         if (string.IsNullOrEmpty(value))
         {
-            value = (await GetConfigQueryable().FirstAsync(u => u.Code == code))?.Value;
+            value = (await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code))?.Value;
             _sysCacheService.Set($"{CacheConst.KeyConfig}{code}", value);
         }
         if (string.IsNullOrWhiteSpace(value)) return default;
@@ -179,7 +178,7 @@ public class SysConfigService : IDynamicApiController, ITransient
     [NonAction]
     public async Task UpdateConfigValue(string code, string value)
     {
-        var config = await GetConfigQueryable().FirstAsync(u => u.Code == code);
+        var config = await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code);
         if (config == null) return;
 
         config.Value = value;
@@ -195,7 +194,7 @@ public class SysConfigService : IDynamicApiController, ITransient
     [DisplayName("获取分组列表")]
     public async Task<List<string>> GetGroupList()
     {
-        return await GetConfigQueryable()
+        return await _sysConfigRep.AsQueryable()
             .GroupBy(u => u.GroupCode)
             .Select(u => u.GroupCode).ToListAsync();
     }
@@ -235,7 +234,7 @@ public class SysConfigService : IDynamicApiController, ITransient
     {
         foreach (var config in input)
         {
-            var info = await GetConfigQueryable().FirstAsync(c => c.Code == config.Code);
+            var info = await _sysConfigRep.AsQueryable().FirstAsync(c => c.Code == config.Code);
             if (info == null || info.SysFlag == YesNoEnum.Y) continue;
 
             await _sysConfigRep.AsUpdateable().SetColumns(u => u.Value == config.Value).Where(u => u.Code == config.Code).ExecuteCommandAsync();
@@ -256,7 +255,7 @@ public class SysConfigService : IDynamicApiController, ITransient
         tenant ??= await _sysTenantRep.GetFirstAsync(u => u.Id == SqlSugarConst.DefaultTenantId);
         _ = tenant ?? throw Oops.Oh(ErrorCodeEnum.D1002);
 
-        var wayList = await _sysConfigRep.Context.Queryable<SysUserRegWay>().ClearFilter()
+        var wayList = await _sysConfigRep.Context.Queryable<SysUserRegWay>()
             .Where(u => u.TenantId == tenant.Id)
             .Select(u => new { Label = u.Name, Value = u.Id })
             .ToListAsync();
@@ -301,16 +300,6 @@ public class SysConfigService : IDynamicApiController, ITransient
         await _sysConfigRep.Context.Updateable(tenant).ExecuteCommandAsync();
     }
 
-    /// <summary>
-    /// 获取参数配置查询器
-    /// </summary>
-    /// <returns></returns>
-    [NonAction]
-    public ISugarQueryable<SysConfig> GetConfigQueryable()
-    {
-        return _sysConfigRep.AsQueryable().ClearFilter().Where(u => u.SysFlag == YesNoEnum.Y || u.TenantId == _userManager.TenantId);
-    }
-
     private void Remove(SysConfig config)
     {
         _sysCacheService.Remove($"{CacheConst.KeyConfig}Value:{config.Code}");

+ 251 - 0
Admin.NET/Admin.NET.Core/Service/Config/SysTenantConfigService.cs

@@ -0,0 +1,251 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统租户配置参数服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 440, Description = "租户配置参数")]
+public class SysConfigTenantService : IDynamicApiController, ITransient
+{
+    private readonly SysCacheService _sysCacheService;
+    private readonly SqlSugarRepository<SysConfigTenant> _sysConfigRep;
+
+    public SysConfigTenantService(SysCacheService sysCacheService,
+        SqlSugarRepository<SysConfigTenant> sysConfigRep)
+    {
+        _sysCacheService = sysCacheService;
+        _sysConfigRep = sysConfigRep;
+    }
+
+    /// <summary>
+    /// 获取配置参数分页列表 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取配置参数分页列表")]
+    public async Task<SqlSugarPagedList<SysConfigTenant>> Page(PageConfigTenantInput input)
+    {
+        return await _sysConfigRep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
+            .OrderBuilder(input)
+            .ToPagedListAsync(input.Page, input.PageSize);
+    }
+
+    /// <summary>
+    /// 获取配置参数列表 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取配置参数列表")]
+    public async Task<List<SysConfigTenant>> List(PageConfigTenantInput input)
+    {
+        return await _sysConfigRep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
+            .ToListAsync();
+    }
+
+    /// <summary>
+    /// 增加配置参数 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    [DisplayName("增加配置参数")]
+    public async Task AddConfig(AddConfigTenantInput input)
+    {
+        var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
+
+        await _sysConfigRep.InsertAsync(input.Adapt<SysConfigTenant>());
+    }
+
+    /// <summary>
+    /// 更新配置参数 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    [DisplayName("更新配置参数")]
+    [UnitOfWork]
+    public async Task UpdateConfig(UpdateConfigTenantInput input)
+    {
+        var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
+
+        //// 若修改国密SM2密匙则密码重新加密
+        //if (input.Code == ConfigConst.SysSM2Key && CryptogramUtil.CryptoType == CryptogramEnum.SM2.ToString())
+        //{
+        //    var sysUserRep = _sysConfigRep.ChangeRepository<SqlSugarRepository<SysUser>>();
+        //    var sysUsers = await sysUserRep.AsQueryable().Select(u => new { u.Id, u.Password }).ToListAsync();
+        //    foreach(var user in sysUsers)
+        //    {
+        //        user.Password = CryptogramUtil.Encrypt(CryptogramUtil.Decrypt(user.Password));
+        //    }
+        //    await sysUserRep.AsUpdateable(sysUsers).UpdateColumns(u => new { u.Password }).ExecuteCommandAsync();
+        //}
+
+        var config = input.Adapt<SysConfigTenant>();
+        await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync();
+
+        RemoveConfigCache(config);
+    }
+
+    /// <summary>
+    /// 删除配置参数 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    [DisplayName("删除配置参数")]
+    public async Task DeleteConfig(DeleteConfigTenantInput input)
+    {
+        var config = await _sysConfigRep.GetByIdAsync(input.Id);
+        // 禁止删除系统参数
+        if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001);
+
+        await _sysConfigRep.DeleteAsync(config);
+
+        RemoveConfigCache(config);
+    }
+
+    /// <summary>
+    /// 批量删除配置参数 🔖
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
+    [DisplayName("批量删除配置参数")]
+    public async Task BatchDeleteConfig(List<long> ids)
+    {
+        foreach (var id in ids)
+        {
+            var config = await _sysConfigRep.GetByIdAsync(id);
+            // 禁止删除系统参数
+            if (config.SysFlag == YesNoEnum.Y) continue;
+
+            await _sysConfigRep.DeleteAsync(config);
+
+            RemoveConfigCache(config);
+        }
+    }
+
+    /// <summary>
+    /// 获取配置参数详情 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取配置参数详情")]
+    public async Task<SysConfigTenant> GetDetail([FromQuery] ConfigTenantInput input)
+    {
+        return await _sysConfigRep.GetByIdAsync(input.Id);
+    }
+
+    /// <summary>
+    /// 根据Code获取配置参数
+    /// </summary>
+    /// <param name="code"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<SysConfigTenant> GetConfig(string code)
+    {
+        return await _sysConfigRep.GetFirstAsync(u => u.Code == code);
+    }
+
+    /// <summary>
+    /// 根据Code获取配置参数值 🔖
+    /// </summary>
+    /// <param name="code"></param>
+    /// <returns></returns>
+    [DisplayName("根据Code获取配置参数值")]
+    public async Task<string> GetConfigValueByCode(string code)
+    {
+        return await GetConfigValueByCode<string>(code);
+    }
+
+    /// <summary>
+    /// 获取配置参数值
+    /// </summary>
+    /// <param name="code"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<T> GetConfigValueByCode<T>(string code)
+    {
+        if (string.IsNullOrWhiteSpace(code)) return default;
+
+        var value = _sysCacheService.Get<string>($"{CacheConst.KeyConfig}{code}");
+        if (string.IsNullOrEmpty(value))
+        {
+            value = (await _sysConfigRep.CopyNew().GetFirstAsync(u => u.Code == code))?.Value;
+            _sysCacheService.Set($"{CacheConst.KeyConfig}{code}", value);
+        }
+        if (string.IsNullOrWhiteSpace(value)) return default;
+        return (T)Convert.ChangeType(value, typeof(T));
+    }
+
+    /// <summary>
+    /// 更新配置参数值
+    /// </summary>
+    /// <param name="code"></param>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task UpdateConfigValue(string code, string value)
+    {
+        var config = await _sysConfigRep.GetFirstAsync(u => u.Code == code);
+        if (config == null) return;
+
+        config.Value = value;
+        await _sysConfigRep.AsUpdateable(config).ExecuteCommandAsync();
+
+        RemoveConfigCache(config);
+    }
+
+    /// <summary>
+    /// 获取分组列表 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取分组列表")]
+    public async Task<List<string>> GetGroupList()
+    {
+        return await _sysConfigRep.AsQueryable()
+            .GroupBy(u => u.GroupCode)
+            .Select(u => u.GroupCode).ToListAsync();
+    }
+
+    /// <summary>
+    /// 批量更新配置参数值 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost]
+    [DisplayName("批量更新配置参数值")]
+    public async Task BatchUpdateConfig(List<BatchConfigTenantInput> input)
+    {
+        foreach (var config in input)
+        {
+            var configInfo = await _sysConfigRep.GetFirstAsync(u => u.Code == config.Code);
+            if (configInfo == null) continue;
+
+            await _sysConfigRep.AsUpdateable().SetColumns(u => u.Value == config.Value).Where(u => u.Code == config.Code).ExecuteCommandAsync();
+            RemoveConfigCache(configInfo);
+        }
+    }
+
+    /// <summary>
+    /// 清除配置缓存
+    /// </summary>
+    /// <param name="config"></param>
+    private void RemoveConfigCache(SysConfigTenant config)
+    {
+        _sysCacheService.Remove($"{CacheConst.KeyConfig}Value:{config.Code}");
+        _sysCacheService.Remove($"{CacheConst.KeyConfig}Remark:{config.Code}");
+        _sysCacheService.Remove($"{CacheConst.KeyConfig}{config.GroupCode}:GroupWithCache");
+        _sysCacheService.Remove($"{CacheConst.KeyConfig}{config.Code}");
+    }
+}

+ 10 - 5
Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs

@@ -18,9 +18,14 @@ public class PageDictDataInput : BasePageInput
     public long DictTypeId { get; set; }
 
     /// <summary>
-    /// 字典文本
+    /// 文本
     /// </summary>
-    public string Label { get; set; }
+    public string Lable { get; set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    public string Code { get; set; }
 }
 
 public class AddDictDataInput : SysDictData
@@ -47,10 +52,10 @@ public class GetDataDictDataInput
 public class QueryDictDataInput
 {
     /// <summary>
-    /// 编码
+    /// 字典值
     /// </summary>
-    [Required(ErrorMessage = "字典唯一编码不能为空")]
-    public string Code { get; set; }
+    [Required(ErrorMessage = "字典不能为空")]
+    public string Value { get; set; }
 
     /// <summary>
     /// 状态

+ 4 - 9
Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs

@@ -25,6 +25,10 @@ public class PageDictTypeInput : BasePageInput
 
 public class AddDictTypeInput : SysDictType
 {
+    /// <summary>
+    /// 是否是内置字典(Y-是,N-否)
+    /// </summary>
+    public override YesNoEnum SysFlag { get; set; } = YesNoEnum.N;
 }
 
 public class UpdateDictTypeInput : AddDictTypeInput
@@ -35,15 +39,6 @@ public class DeleteDictTypeInput : BaseIdInput
 {
 }
 
-public class DictTypeMoveInput : BaseIdInput
-{
-    /// <summary>
-    /// 租户
-    /// </summary>
-    [Required(ErrorMessage = "租户不能为空")]
-    public long? TenantId { get; set; }
-}
-
 public class GetDataDictTypeInput
 {
     /// <summary>

+ 90 - 51
Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs

@@ -9,21 +9,25 @@ namespace Admin.NET.Core.Service;
 /// <summary>
 /// 系统字典值服务 🧩
 /// </summary>
-[ApiDescriptionSettings(Order = 420)]
+[ApiDescriptionSettings(Order = 420, Description = "系统字典值")]
 public class SysDictDataService : IDynamicApiController, ITransient
 {
     private readonly SqlSugarRepository<SysDictData> _sysDictDataRep;
+    public readonly ISugarQueryable<SysDictData> VSysDictData;
     private readonly SysCacheService _sysCacheService;
     private readonly UserManager _userManager;
 
-    public SysDictDataService(
-        SqlSugarRepository<SysDictData> sysDictDataRep,
+    public SysDictDataService(SqlSugarRepository<SysDictData> sysDictDataRep,
         SysCacheService sysCacheService,
         UserManager userManager)
     {
         _userManager = userManager;
         _sysDictDataRep = sysDictDataRep;
         _sysCacheService = sysCacheService;
+        VSysDictData = _sysDictDataRep.Context.UnionAll(
+            _sysDictDataRep.AsQueryable(),
+            _sysDictDataRep.Change<SysDictDataTenant>().AsQueryable()
+                .Select<SysDictData>());
     }
 
     /// <summary>
@@ -34,10 +38,11 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("获取字典值分页列表")]
     public async Task<SqlSugarPagedList<SysDictData>> Page(PageDictDataInput input)
     {
-        return await GetDictValueQueryable()
+        return await VSysDictData
             .Where(u => u.DictTypeId == input.DictTypeId)
-            .WhereIF(!string.IsNullOrEmpty(input.Label?.Trim()), u => u.Value.Contains(input.Label))
-            .OrderBy(u => new { u.OrderNo, Code = u.Value })
+            .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
+            .WhereIF(!string.IsNullOrEmpty(input.Lable?.Trim()), u => u.Label.Contains(input.Lable))
+            .OrderBy(u => new { u.OrderNo, u.Code })
             .ToPagedListAsync(input.Page, input.PageSize);
     }
 
@@ -48,7 +53,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("获取字典值列表")]
     public async Task<List<SysDictData>> GetList([FromQuery] GetDataDictDataInput input)
     {
-        return await GetDictValueQueryable().Where(u => u.DictTypeId == input.DictTypeId).ToListAsync();
+        return await GetDictDataListByDictTypeId(input.DictTypeId);
     }
 
     /// <summary>
@@ -60,16 +65,16 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("增加字典值")]
     public async Task AddDictData(AddDictDataInput input)
     {
-        var isExist = await GetDictValueQueryable().AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId);
+        var isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003);
 
         var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(input.DictTypeId);
-        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
 
-        var dictTypeCode = await GetDictValueQueryable().Where(u => u.DictTypeId == input.DictTypeId).Select(u => u.DictType.Code).FirstAsync();
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
 
-        await _sysDictDataRep.InsertAsync(input.Adapt<SysDictData>());
+        dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
+        await _sysDictDataRep.Context.Insertable(dictData).ExecuteCommandAsync();
     }
 
     /// <summary>
@@ -82,19 +87,18 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("更新字典值")]
     public async Task UpdateDictData(UpdateDictDataInput input)
     {
-        var isExist = await GetDictValueQueryable().AnyAsync(u => u.Id == input.Id);
+        var isExist = await VSysDictData.AnyAsync(u => u.Id == input.Id);
         if (!isExist) throw Oops.Oh(ErrorCodeEnum.D3004);
 
-        isExist = await GetDictValueQueryable().AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId && u.Id != input.Id);
+        isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId && u.Id != input.Id);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003);
 
         var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(input.DictTypeId);
-        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
 
-        var dictTypeCode = await GetDictValueQueryable().Where(u => u.DictTypeId == input.DictTypeId).Select(u => u.DictType.Code).FirstAsync();
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
-
-        await _sysDictDataRep.UpdateAsync(input.Adapt<SysDictData>());
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
+        dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
+        await _sysDictDataRep.Context.Updateable(dictData).ExecuteCommandAsync();
     }
 
     /// <summary>
@@ -107,15 +111,14 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("删除字典值")]
     public async Task DeleteDictData(DeleteDictDataInput input)
     {
-        var dictData = await _sysDictDataRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
-
-        var dictTypeCode = await GetDictValueQueryable().Where(u => u.DictTypeId == dictData.DictTypeId).Select(u => u.DictType.Code).FirstAsync();
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
+        var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
 
         var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(dictData.DictTypeId);
         if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
 
-        await _sysDictDataRep.DeleteAsync(dictData);
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
+        dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
+        await _sysDictDataRep.Context.Deleteable(entity).ExecuteCommandAsync();
     }
 
     /// <summary>
@@ -126,7 +129,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("获取字典值详情")]
     public async Task<SysDictData> GetDetail([FromQuery] DictDataInput input)
     {
-        return await _sysDictDataRep.GetByIdAsync(input.Id);
+        return (await VSysDictData.FirstAsync(u => u.Id == input.Id))?.Adapt<SysDictData>();
     }
 
     /// <summary>
@@ -138,13 +141,27 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("修改字典值状态")]
     public async Task SetStatus(DictDataInput input)
     {
-        var dictData = await _sysDictDataRep.AsQueryable().FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
+        var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
 
-        var dictTypeCode = await GetDictValueQueryable().Where(u => u.DictTypeId == dictData.Id).Select(u => u.DictType.Code).FirstAsync();
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
+        var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(dictData.DictTypeId);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
+
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
 
         dictData.Status = input.Status;
-        await _sysDictDataRep.AsUpdateable(dictData).UpdateColumns(u => new { u.Status }, true).ExecuteCommandAsync();
+        dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
+        await _sysDictDataRep.Context.Updateable(entity).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 根据字典类型Id获取字典值集合
+    /// </summary>
+    /// <param name="dictTypeId"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<List<SysDictData>> GetDictDataListByDictTypeId(long dictTypeId)
+    {
+        return await GetDataListByIdOrCode(dictTypeId, null);
     }
 
     /// <summary>
@@ -155,12 +172,42 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("根据字典类型编码获取字典值集合")]
     public async Task<List<SysDictData>> GetDataList(string code)
     {
-        var cacheKey = $"{CacheConst.KeyDict}{code}";
-        var dictDataList = _sysCacheService.Get<List<SysDictData>>(cacheKey);
-        if (dictDataList != null) return dictDataList;
+        return await GetDataListByIdOrCode(null, code);
+    }
 
-        dictDataList = await GetDictValueQueryable().Where(u => u.DictType.Code == code && u.Status == StatusEnum.Enable && u.DictType.Status == StatusEnum.Enable).ToListAsync();
-        _sysCacheService.Set(cacheKey, dictDataList);
+    /// <summary>
+    /// 获取字典值集合 🔖
+    /// </summary>
+    /// <param name="typeId"></param>
+    /// <param name="code"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<List<SysDictData>> GetDataListByIdOrCode(long? typeId, string code)
+    {
+        if (string.IsNullOrWhiteSpace(code) && typeId == null ||
+            !string.IsNullOrWhiteSpace(code) && typeId != null)
+            throw Oops.Oh(ErrorCodeEnum.D3011);
+
+        var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code)
+            .WhereIF(typeId != null, u => u.Id == typeId)
+            .FirstAsync();
+        if (dictType == null) return null;
+
+        var dictDataList = _sysCacheService.Get<List<SysDictData>>($"{CacheConst.KeyDict}{dictType.Code}");
+        if (dictDataList == null)
+        {
+
+            dictDataList = await VSysDictData.InnerJoin<SysDictType>((u, a) => u.DictTypeId == a.Id)
+                .Where(u => u.DictTypeId == dictType.Id)
+                .Select((u, a) => new SysDictData
+                {
+                    Status = u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable,
+                }, true)
+                .OrderBy(u => new { u.OrderNo, u.Value, u.Code })
+                .ToListAsync();
+            _sysCacheService.Set($"{CacheConst.KeyDict}{dictType.Code}", dictDataList);
+        }
         return dictDataList;
     }
 
@@ -172,10 +219,9 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("根据查询条件获取字典值集合")]
     public async Task<List<SysDictData>> GetDataList([FromQuery] QueryDictDataInput input)
     {
-        return await GetDictValueQueryable()
-            .Where(u => u.DictType.Code == input.Code)
-            .WhereIF(input.Status > 0, u => u.Status == (StatusEnum)input.Status)
-            .ToListAsync();
+        var dataList = await GetDataList(input.Value);
+        if (input.Status.HasValue) return dataList.Where(u => u.Status == (StatusEnum)input.Status.Value).ToList();
+        return dataList;
     }
 
     /// <summary>
@@ -186,19 +232,12 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [NonAction]
     public async Task DeleteDictData(long dictTypeId)
     {
-        var dictTypeCode = await GetDictValueQueryable().Where(u => u.DictTypeId == dictTypeId).Select(u => u.DictType.Code).FirstAsync();
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
-        await _sysDictDataRep.DeleteAsync(u => u.DictTypeId == dictTypeId);
-    }
+        var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable().Where(u => u.Id == dictTypeId).FirstAsync();
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType?.Code}");
 
-    /// <summary>
-    /// 获取字典值查询器
-    /// </summary>
-    /// <returns></returns>
-    [NonAction]
-    public ISugarQueryable<SysDictData> GetDictValueQueryable()
-    {
-        return _sysDictDataRep.AsQueryable().ClearFilter()
-            .Where(u => u.DictType.SysFlag == YesNoEnum.Y || u.TenantId == _userManager.TenantId);
+        if (dictType?.IsTenant == YesNoEnum.Y)
+            await _sysDictDataRep.Change<SysDictDataTenant>().DeleteAsync(u => u.DictTypeId == dictTypeId);
+        else
+            await _sysDictDataRep.DeleteAsync(u => u.DictTypeId == dictTypeId);
     }
 }

+ 9 - 14
Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs

@@ -9,7 +9,7 @@ namespace Admin.NET.Core.Service;
 /// <summary>
 /// 系统字典类型服务 🧩
 /// </summary>
-[ApiDescriptionSettings(Order = 430)]
+[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")]
 public class SysDictTypeService : IDynamicApiController, ITransient
 {
     private readonly SqlSugarRepository<SysDictType> _sysDictTypeRep;
@@ -36,7 +36,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     public async Task<SqlSugarPagedList<SysDictType>> Page(PageDictTypeInput input)
     {
         return await _sysDictTypeRep.AsQueryable()
-            .WhereIF(!_userManager.SuperAdmin, u => u.SysFlag == YesNoEnum.N)
+            .WhereIF(!_userManager.SuperAdmin, u => u.IsTenant == YesNoEnum.Y)
             .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
             .WhereIF(!string.IsNullOrEmpty(input.Name?.Trim()), u => u.Name.Contains(input.Name))
             .OrderBy(u => new { u.OrderNo, u.Code })
@@ -61,11 +61,8 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("获取字典类型-值列表")]
     public async Task<List<SysDictData>> GetDataList([FromQuery] GetDataDictTypeInput input)
     {
-        return await _sysDictTypeRep.AsQueryable()
-            .Where(u => u.Code == input.Code)
-            .InnerJoin<SysDictData>((u, w) => u.Id == w.DictTypeId)
-            .Select((u, w) => w)
-            .ToListAsync() ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Code == input.Code) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        return await _sysDictDataService.GetDictDataListByDictTypeId(dictType.Id);
     }
 
     /// <summary>
@@ -77,9 +74,8 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("添加字典类型")]
     public async Task AddDictType(AddDictTypeInput input)
     {
-        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
-
         if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006);
+        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
 
         var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
@@ -97,12 +93,12 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("更新字典类型")]
     public async Task UpdateDictType(UpdateDictTypeInput input)
     {
-        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
-
         var dict = await _sysDictTypeRep.GetFirstAsync(x => x.Id == input.Id);
+        if (dict.IsTenant != input.IsTenant) throw Oops.Oh(ErrorCodeEnum.D3012);
         if (dict == null) throw Oops.Oh(ErrorCodeEnum.D3000);
 
         if (dict.Code.ToLower().EndsWith("enum") && input.Code != dict.Code) throw Oops.Oh(ErrorCodeEnum.D3007);
+        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
 
         var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
@@ -150,6 +146,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     public async Task SetStatus(DictTypeInput input)
     {
         var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
 
         _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
 
@@ -165,9 +162,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     public async Task<dynamic> GetAllDictList()
     {
         var ds = await _sysDictTypeRep.AsQueryable()
-            .InnerJoin<SysDictData>((u, w) => u.Id == w.DictTypeId).ClearFilter()
-            .Where((u, w) => !string.IsNullOrWhiteSpace(w.Value))
-            .Where((u, w) => u.SysFlag == YesNoEnum.Y || (u.SysFlag == YesNoEnum.N && w.TenantId == _userManager.TenantId))
+            .InnerJoin(_sysDictDataService.VSysDictData, (u, w) => u.Id == w.DictTypeId)
             .Select((u, w) => new
             {
                 TypeCode = u.Code,

+ 9 - 1
Web/src/views/system/dict/component/editDictType.vue

@@ -19,7 +19,7 @@
 							<el-input v-model="state.ruleForm.code" placeholder="字典编码" clearable />
 						</el-form-item>
 					</el-col>
-					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" v-if="userInfo.accountType === AccountTypeEnum.NUMBER_999">
+					<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="userInfo.accountType === AccountTypeEnum.NUMBER_999">
 						<el-form-item label="内置参数" prop="sysFlag" :rules="[{ required: true, message: '内置参数不能为空', trigger: 'blur' }]">
 							<el-radio-group v-model="state.ruleForm.sysFlag" :disabled="state.ruleForm.sysFlag == 1 && state.ruleForm.id != undefined">
 								<el-radio :value="1">是</el-radio>
@@ -27,6 +27,14 @@
 							</el-radio-group>
 						</el-form-item>
 					</el-col>
+					<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="userInfo.accountType === AccountTypeEnum.NUMBER_999">
+						<el-form-item label="租户字典" prop="isTenant" :rules="[{ required: true, message: '租户字典不能为空', trigger: 'blur' }]">
+							<el-radio-group v-model="state.ruleForm.isTenant" :disabled="state.ruleForm.id">
+								<el-radio :value="1">是</el-radio>
+								<el-radio :value="2">否</el-radio>
+							</el-radio-group>
+						</el-form-item>
+					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						<el-form-item label="状态">
 							<el-radio-group v-model="state.ruleForm.status">

+ 5 - 0
Web/src/views/system/dict/index.vue

@@ -30,6 +30,11 @@
                 <g-sys-dict v-model="scope.row.sysFlag" code="YesNoEnum" />
 							</template>
 						</el-table-column>
+						<el-table-column prop="isTenant" label="租户字典" min-width="70" align="center" show-overflow-tooltip v-if="userInfo.accountType === AccountTypeEnum.NUMBER_999">
+							<template #default="scope">
+                <g-sys-dict v-model="scope.row.isTenant" code="YesNoEnum" />
+							</template>
+						</el-table-column>
 						<el-table-column prop="status" label="状态" width="70" align="center" show-overflow-tooltip>
 							<template #default="scope">
                 <g-sys-dict v-model="scope.row.status" code="StatusEnum" />