Эх сурвалжийг харах

Merge remote-tracking branch 'upstream/next' into next

FunCoder 2 жил өмнө
parent
commit
ed5b92df1f
100 өөрчлөгдсөн 1697 нэмэгдсэн , 277 устгасан
  1. 1 1
      .gitignore
  2. 2 0
      Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj
  3. 2 12
      Admin.NET/Admin.NET.Application/Configuration/Swagger.json
  4. 2 2
      Admin.NET/Admin.NET.Application/Const/ApplicationConst.cs
  5. 5 5
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  6. 3 1
      Admin.NET/Admin.NET.Core/Attribute/IdempotentAttribute.cs
  7. 1 0
      Admin.NET/Admin.NET.Core/Attribute/MaxValueAttribute.cs
  8. 1 0
      Admin.NET/Admin.NET.Core/Attribute/MinValueAttribute.cs
  9. 1 1
      Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs
  10. 4 4
      Admin.NET/Admin.NET.Core/Const/CommonConst.cs
  11. 14 14
      Admin.NET/Admin.NET.Core/Entity/EntityBase.cs
  12. 15 1
      Admin.NET/Admin.NET.Core/Entity/SysLdap.cs
  13. 1 1
      Admin.NET/Admin.NET.Core/EventBus/RedisQueue.cs
  14. 2 2
      Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs
  15. 10 5
      Admin.NET/Admin.NET.Core/Job/LogJob.cs
  16. 10 3
      Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs
  17. 18 4
      Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs
  18. 4 4
      Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs
  19. 12 14
      Admin.NET/Admin.NET.Core/Logging/ElasticSearchSetup.cs
  20. 3 2
      Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs
  21. 1 1
      Admin.NET/Admin.NET.Core/Service/APIJSON/APIJSONService.cs
  22. 5 1
      Admin.NET/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs
  23. 2 2
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  24. 142 41
      Admin.NET/Admin.NET.Core/Service/Auth/SysLdapService.cs
  25. 1 1
      Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs
  26. 2 2
      Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs
  27. 9 7
      Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs
  28. 1 1
      Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs
  29. 1 1
      Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs
  30. 1 1
      Admin.NET/Admin.NET.Core/Service/Const/SysConstService.cs
  31. 58 0
      Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs
  32. 61 1
      Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs
  33. 1 1
      Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs
  34. 1 1
      Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs
  35. 1 1
      Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs
  36. 9 5
      Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs
  37. 8 8
      Admin.NET/Admin.NET.Core/Service/Job/JobClusterServer.cs
  38. 1 1
      Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs
  39. 1 1
      Admin.NET/Admin.NET.Core/Service/Logging/SysLogDiffService.cs
  40. 1 1
      Admin.NET/Admin.NET.Core/Service/Logging/SysLogExService.cs
  41. 1 1
      Admin.NET/Admin.NET.Core/Service/Logging/SysLogOpService.cs
  42. 1 1
      Admin.NET/Admin.NET.Core/Service/Logging/SysLogVisService.cs
  43. 1 1
      Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs
  44. 1 1
      Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs
  45. 1 4
      Admin.NET/Admin.NET.Core/Service/Message/SysMessageService.cs
  46. 1 1
      Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs
  47. 1 1
      Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs
  48. 1 1
      Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs
  49. 1 1
      Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs
  50. 9 9
      Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs
  51. 2 2
      Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs
  52. 1 1
      Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs
  53. 1 1
      Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs
  54. 1 1
      Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs
  55. 3 3
      Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs
  56. 1 1
      Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs
  57. 4 4
      Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs
  58. 1 1
      Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs
  59. 5 0
      Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs
  60. 5 0
      Admin.NET/Admin.NET.Core/Service/User/Dto/UserOutput.cs
  61. 68 0
      Admin.NET/Admin.NET.Core/Service/User/SysUserLdapService.cs
  62. 18 4
      Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs
  63. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs
  64. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs
  65. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs
  66. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs
  67. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs
  68. 28 5
      Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs
  69. 20 19
      Admin.NET/Admin.NET.Core/Util/ComputerUtil.cs
  70. 44 38
      Admin.NET/Admin.NET.Core/Util/VerifyFileExtensionName.cs
  71. 5 4
      Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs
  72. 8 2
      Admin.NET/Admin.NET.Web.Core/Startup.cs
  73. 1 1
      Admin.NET/Admin.NET.Web.Entry/wwwroot/Template/Service.cs.vm
  74. 13 6
      Admin.NET/Admin.NET.Web.Entry/wwwroot/Template/index.vue.vm
  75. 7 0
      Admin.NET/Admin.NET.sln
  76. 28 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Admin.NET.Plugin.DingTalk.csproj
  77. 36 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Const/DingTalkConst.cs
  78. 10 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/DingTalk.json
  79. 78 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkUser.cs
  80. 28 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Enum/DingTalkConversationTypeEnum.cs
  81. 24 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/GlobalUsings.cs
  82. 32 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Option/DingTalkOptions.cs
  83. 209 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/DingTalkService.cs
  84. 43 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkBaseResponse.cs
  85. 38 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCardData.cs
  86. 40 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpFieldDataVo.cs
  87. 33 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpRosterFieldVo.cs
  88. 33 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkFieldValueVo.cs
  89. 115 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsInput.cs
  90. 25 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsOutput.cs
  91. 17 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsResult.cs
  92. 32 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListInput.cs
  93. 26 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListOutput.cs
  94. 31 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesRosterListInput.cs
  95. 49 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkToken.cs
  96. 63 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs
  97. 26 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs
  98. 2 2
      Admin.NET/Plugins/Admin.NET.Plugin.GoView/Const/GoViewConst.cs
  99. 1 1
      Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewPro.cs
  100. 1 1
      Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewProData.cs

+ 1 - 1
.gitignore

@@ -32,8 +32,8 @@ bin-release/
 dist/
 node_modules/
 /Web/package-lock.json
-.DS_Store
 /Web/public/config.js
 /Web/stats.html
 .vs
 .idea
+.DS_Store

+ 2 - 0
Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj

@@ -33,6 +33,8 @@
 
   <ItemGroup>
     <ProjectReference Include="..\Admin.NET.Core\Admin.NET.Core.csproj" />
+    <ProjectReference Include="..\Plugins\Admin.NET.Plugin.DingTalk\Admin.NET.Plugin.DingTalk.csproj" />
+    <ProjectReference Include="..\Plugins\Admin.NET.Plugin.GoView\Admin.NET.Plugin.GoView.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 12
Admin.NET/Admin.NET.Application/Configuration/Swagger.json

@@ -7,24 +7,14 @@
       {
         "Group": "Default",
         "Title": "Admin.NET 通用权限开发平台",
-        "Description": "让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术<br/>",
+        "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。",
         "Version": "1.0.0"
-        //"TermsOfService": "https://dotnetchina.gitee.io/furion/",
-        //"Contact": {
-        //  "Name": "zuohuaijun",
-        //  "Url": "https://gitee.com/zuohuaijun/Admin.NET"
-        //}
       },
       {
         "Group": "All Groups",
         "Title": "所有接口",
-        "Description": "让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术<br/>",
+        "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。",
         "Version": "1.0.0"
-        //"TermsOfService": "https://dotnetchina.gitee.io/furion/",
-        //"Contact": {
-        //  "Name": "zuohuaijun",
-        //  "Url": "https://gitee.com/zuohuaijun/Admin.NET"
-        //}
       }
     ],
     "DefaultGroupName": "Default", // 默认分组名

+ 2 - 2
Admin.NET/Admin.NET.Application/Const/ApplicationConst.cs

@@ -6,7 +6,7 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-namespace Admin.NET.Application.Const;
+namespace Admin.NET.Application;
 
 /// <summary>
 /// 业务应用相关常量
@@ -16,5 +16,5 @@ public class ApplicationConst
     /// <summary>
     /// API分组名称
     /// </summary>
-    public const string GroupName = "业务应用";
+    public const string GroupName = "xxx业务应用";
 }

+ 5 - 5
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -21,19 +21,19 @@
     <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="6.0.15" />
     <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="6.0.15" />
     <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
-    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.2.16" />
-    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.2.16" />
-    <PackageReference Include="Furion.Pure" Version="4.9.2.16" />
+    <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.13.5" />
+    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.2.17" />
+    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.2.17" />
+    <PackageReference Include="Furion.Pure" Version="4.9.2.17" />
     <PackageReference Include="IPTools.China" Version="1.6.0" />
     <PackageReference Include="IPTools.International" Version="1.6.0" />
     <PackageReference Include="Lazy.Captcha.Core" Version="2.0.6" />
     <PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.1" />
     <PackageReference Include="Magicodes.IE.Pdf" Version="2.7.5.1" />
     <PackageReference Include="Magicodes.IE.Word" Version="2.7.5.1" />
-    <PackageReference Include="MailKit" Version="4.4.0" />
+    <PackageReference Include="MailKit" Version="4.5.0" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="6.0.29" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.29" />
-    <PackageReference Include="NEST" Version="7.17.5" />
     <PackageReference Include="NewLife.Redis" Version="5.6.2024.402" />
     <PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
     <PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.1.9" />

+ 3 - 1
Admin.NET/Admin.NET.Core/Attribute/IdempotentAttribute.cs

@@ -13,6 +13,8 @@ namespace Admin.NET.Core;
 /// <summary>
 /// 防止重复请求过滤器特性
 /// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
 public class IdempotentAttribute : Attribute, IAsyncActionFilter
 {
     /// <summary>
@@ -54,7 +56,7 @@ public class IdempotentAttribute : Attribute, IAsyncActionFilter
         }
 
         var cacheKey = MD5Encryption.Encrypt($"{CacheKey}{path}{userId}{parameters}");
-        var sysCacheService = App.GetService<SysCacheService>();
+        var sysCacheService = App.GetRequiredService<SysCacheService>();
         if (sysCacheService.ExistKey(cacheKey))
         {
             if (ThrowBah) throw Oops.Oh(Message);

+ 1 - 0
Admin.NET/Admin.NET.Core/Attribute/MaxValueAttribute.cs

@@ -11,6 +11,7 @@ namespace Admin.NET.Core;
 /// <summary>
 /// 最大值校验
 /// </summary>
+[SuppressSniffer]
 public class MaxValueAttribute : ValidationAttribute
 {
     private double MaxValue { get; }

+ 1 - 0
Admin.NET/Admin.NET.Core/Attribute/MinValueAttribute.cs

@@ -11,6 +11,7 @@ namespace Admin.NET.Core;
 /// <summary>
 /// 最小值校验
 /// </summary>
+[SuppressSniffer]
 public class MinValueAttribute : ValidationAttribute
 {
     private double MinValue { get; set; }

+ 1 - 1
Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs

@@ -16,7 +16,7 @@ public class SqlSugarCache : ICacheService
     /// <summary>
     /// 系统缓存服务
     /// </summary>
-    private static readonly SysCacheService _cache = App.GetService<SysCacheService>();
+    private static readonly SysCacheService _cache = App.GetRequiredService<SysCacheService>();
 
     public void Add<V>(string key, V value)
     {

+ 4 - 4
Admin.NET/Admin.NET.Core/Const/CommonConst.cs

@@ -69,10 +69,10 @@ public class CommonConst
     /// </summary>
     public const string SysAdminRole = "sys_admin";
 
-    /// <summary>
-    /// 开启全局脱敏处理(默认不开启)
-    /// </summary>
-    public static bool SysSensitiveDetection = false;
+    ///// <summary>
+    ///// 开启全局脱敏处理(默认不开启)
+    ///// </summary>
+    //public static bool SysSensitiveDetection = false;
 
     /// <summary>
     /// 开启域登录验证

+ 14 - 14
Admin.NET/Admin.NET.Core/Entity/EntityBase.cs

@@ -44,13 +44,13 @@ public abstract class EntityBase : EntityBaseId, IDeletedFilter
     [SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
     public virtual long? CreateUserId { get; set; }
 
-    /// <summary>
-    /// 创建者
-    /// </summary>
-    [Newtonsoft.Json.JsonIgnore]
-    [System.Text.Json.Serialization.JsonIgnore]
-    [Navigate(NavigateType.OneToOne, nameof(CreateUserId))]
-    public virtual SysUser CreateUser { get; set; }
+    ///// <summary>
+    ///// 创建者
+    ///// </summary>
+    //[Newtonsoft.Json.JsonIgnore]
+    //[System.Text.Json.Serialization.JsonIgnore]
+    //[Navigate(NavigateType.OneToOne, nameof(CreateUserId))]
+    //public virtual SysUser CreateUser { get; set; }
 
     /// <summary>
     /// 创建者姓名
@@ -64,13 +64,13 @@ public abstract class EntityBase : EntityBaseId, IDeletedFilter
     [SugarColumn(ColumnDescription = "修改者Id")]
     public virtual long? UpdateUserId { get; set; }
 
-    /// <summary>
-    /// 修改者
-    /// </summary>
-    [Newtonsoft.Json.JsonIgnore]
-    [System.Text.Json.Serialization.JsonIgnore]
-    [Navigate(NavigateType.OneToOne, nameof(UpdateUserId))]
-    public virtual SysUser UpdateUser { get; set; }
+    ///// <summary>
+    ///// 修改者
+    ///// </summary>
+    //[Newtonsoft.Json.JsonIgnore]
+    //[System.Text.Json.Serialization.JsonIgnore]
+    //[Navigate(NavigateType.OneToOne, nameof(UpdateUserId))]
+    //public virtual SysUser UpdateUser { get; set; }
 
     /// <summary>
     /// 修改者姓名

+ 15 - 1
Admin.NET/Admin.NET.Core/Entity/SysLdap.cs

@@ -54,7 +54,7 @@ public class SysLdap : EntityTenant
     /// </summary>
     [SugarColumn(ColumnDescription = "用户过滤规则", Length = 128)]
     [Required]
-    public virtual string AuthFilter { get; set; } = "sAMAccountName";
+    public virtual string AuthFilter { get; set; } = "sAMAccountName=%s";
 
     /// <summary>
     /// Ldap版本
@@ -62,6 +62,20 @@ public class SysLdap : EntityTenant
     [SugarColumn(ColumnDescription = "Ldap版本")]
     public int Version { get; set; }
 
+    /// <summary>
+    /// 绑定域账号字段属性值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "绑定域账号字段属性值", Length = 32)]
+    [Required]
+    public virtual string BindAttrAccount { get; set; } = "sAMAccountName";
+
+    /// <summary>
+    /// 绑定用户EmployeeId属性值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "绑定用户EmployeeId属性值", Length = 32)]
+    [Required]
+    public virtual string BindAttrEmployeeId { get; set; } = "EmployeeId";
+
     /// <summary>
     /// 状态
     /// </summary>

+ 1 - 1
Admin.NET/Admin.NET.Core/EventBus/RedisQueue.cs

@@ -15,7 +15,7 @@ namespace Admin.NET.Core;
 /// </summary>
 public static class RedisQueue
 {
-    private static ICache _cache = App.GetService<ICache>();
+    private static readonly ICache _cache = App.GetRequiredService<ICache>();
 
     /// <summary>
     /// 获取可信队列,需要确认

+ 2 - 2
Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs

@@ -27,8 +27,8 @@ public class EnumToDictJob : IJob
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
         using var serviceScope = _scopeFactory.CreateScope();
-        var sysEnumService = serviceScope.ServiceProvider.GetService<SysEnumService>();
-        var db = serviceScope.ServiceProvider.GetService<ISqlSugarClient>().CopyNew();
+        var sysEnumService = serviceScope.ServiceProvider.GetRequiredService<SysEnumService>();
+        var db = serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
 
         var enumTypeList = sysEnumService.GetEnumTypeList();
         var enumCodeList = enumTypeList.Select(x => x.TypeName);

+ 10 - 5
Admin.NET/Admin.NET.Core/Job/LogJob.cs

@@ -16,19 +16,21 @@ namespace Admin.NET.Core;
 public class LogJob : IJob
 {
     private readonly IServiceScopeFactory _scopeFactory;
+    private readonly ILogger _logger;
 
-    public LogJob(IServiceScopeFactory scopeFactory)
+    public LogJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
     {
         _scopeFactory = scopeFactory;
+        _logger = loggerFactory.CreateLogger("System.Logging.LoggingMonitor");
     }
 
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
         using var serviceScope = _scopeFactory.CreateScope();
 
-        var logVisRep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysLogVis>>();
-        var logOpRep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysLogOp>>();
-        var logDiffRep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysLogDiff>>();
+        var logVisRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogVis>>();
+        var logOpRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogOp>>();
+        var logDiffRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogDiff>>();
 
         var daysAgo = 30; // 删除30天以前
         await logVisRep.CopyNew().AsDeleteable().Where(u => (DateTime)u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除访问日志
@@ -37,7 +39,10 @@ public class LogJob : IJob
 
         var originColor = Console.ForegroundColor;
         Console.ForegroundColor = ConsoleColor.Yellow;
-        Console.WriteLine("【" + DateTime.Now + "】清系统日志(30天前)");
+        Console.WriteLine("【" + DateTime.Now + "】清系统日志(30天前)");
         Console.ForegroundColor = originColor;
+
+        // 自定义日志
+        _logger.LogInformation("清理系统日志");
     }
 }

+ 10 - 3
Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs

@@ -6,6 +6,8 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
+using Furion.Logging.Extensions;
+
 namespace Admin.NET.Core;
 
 /// <summary>
@@ -16,17 +18,19 @@ namespace Admin.NET.Core;
 public class OnlineUserJob : IJob
 {
     private readonly IServiceScopeFactory _scopeFactory;
+    private readonly ILogger _logger;
 
-    public OnlineUserJob(IServiceScopeFactory scopeFactory)
+    public OnlineUserJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
     {
         _scopeFactory = scopeFactory;
+        _logger = loggerFactory.CreateLogger("System.Logging.LoggingMonitor");
     }
 
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
         using var serviceScope = _scopeFactory.CreateScope();
 
-        var rep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysOnlineUser>>();
+        var rep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysOnlineUser>>();
         await rep.CopyNew().AsDeleteable().ExecuteCommandAsync(stoppingToken);
 
         var originColor = Console.ForegroundColor;
@@ -35,6 +39,9 @@ public class OnlineUserJob : IJob
         Console.ForegroundColor = originColor;
 
         // 缓存租户列表
-        await serviceScope.ServiceProvider.GetService<SysTenantService>().CacheTenant();
+        await serviceScope.ServiceProvider.GetRequiredService<SysTenantService>().CacheTenant();
+
+        // 自定义日志
+        _logger.LogInformation("服务已重启...");
     }
 }

+ 18 - 4
Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs

@@ -36,9 +36,23 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
     public async Task WriteAsync(LogMessage logMsg, bool flush)
     {
         var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString();
-        if (jsonStr == null) return;
-        var loggingMonitor = JSON.Deserialize<dynamic>(jsonStr);
+        if (jsonStr == null)
+        {
+            await _db.Insertable(new SysLogOp
+            {
+                DisplayTitle = "自定义操作日志",
+                LogDateTime = logMsg.LogDateTime,
+                EventId = logMsg.EventId.Id,
+                ThreadId = logMsg.ThreadId,
+                TraceId = logMsg.TraceId,
+                Exception = logMsg.Exception == null ? null : JSON.Serialize(logMsg.Exception),
+                Message = logMsg.Message,
+                LogLevel = logMsg.LogLevel
+            }).ExecuteCommandAsync();
+            return;
+        }
 
+        var loggingMonitor = JSON.Deserialize<dynamic>(jsonStr);
         // 不记录数据校验日志
         if (loggingMonitor.validation != null) return;
 
@@ -69,7 +83,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
         // 捕捉异常,否则会由于 unhandled exception 导致程序崩溃
         try
         {
-            // 记录异常日志发送邮件
+            // 记录异常日志-发送邮件
             if (logMsg.Exception != null || loggingMonitor.exception != null)
             {
                 await _db.Insertable(new SysLogEx
@@ -173,7 +187,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable
         }
         catch (Exception ex)
         {
-            _logger.LogError(ex, ex.Message);
+            _logger.LogError(ex, "操作日志入库");
         }
     }
 

+ 4 - 4
Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs

@@ -6,7 +6,7 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-using Nest;
+using Elastic.Clients.Elasticsearch;
 
 namespace Admin.NET.Core;
 
@@ -16,13 +16,13 @@ namespace Admin.NET.Core;
 public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable
 {
     private readonly IServiceScope _serviceScope;
-    private readonly ElasticClient _esClient;
+    private readonly ElasticsearchClient _esClient;
     private readonly SysConfigService _sysConfigService;
 
     public ElasticSearchLoggingWriter(IServiceScopeFactory scopeFactory)
     {
         _serviceScope = scopeFactory.CreateScope();
-        _esClient = _serviceScope.ServiceProvider.GetRequiredService<ElasticClient>();
+        _esClient = _serviceScope.ServiceProvider.GetRequiredService<ElasticsearchClient>();
         _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
     }
 
@@ -88,7 +88,7 @@ public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable
             CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId),
             TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId)
         };
-        await _esClient.IndexDocumentAsync(sysLogOp);
+        await _esClient.IndexAsync(sysLogOp);
     }
 
     /// <summary>

+ 12 - 14
Admin.NET/Admin.NET.Core/Logging/ElasticSearchSetup.cs

@@ -6,8 +6,8 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-using Elasticsearch.Net;
-using Nest;
+using Elastic.Clients.Elasticsearch;
+using Elastic.Transport;
 
 namespace Admin.NET.Core;
 
@@ -23,23 +23,23 @@ public static class ElasticSearchSetup
 
         var uris = option.ServerUris.Select(u => new Uri(u));
         // 集群
-        var connectionPool = new SniffingConnectionPool(uris);
-        var connectionSettings = new ConnectionSettings(connectionPool).DefaultIndex(option.DefaultIndex);
+        var connectionPool = new StaticNodePool(uris);
+        var connectionSettings = new ElasticsearchClientSettings(connectionPool).DefaultIndex(option.DefaultIndex);
         // 单连接
-        //var connectionSettings = new ConnectionSettings(new SingleNodeConnectionPool(uris.FirstOrDefault())).DefaultIndex(option.DefaultIndex);
+        //var connectionSettings = new ElasticsearchClientSettings(new StaticNodePool(new List<Uri> { uris.FirstOrDefault() })).DefaultIndex(option.DefaultIndex);
 
         // 认证类型
-        if (option.AuthType == ElasticSearchAuthTypeEnum.Basic)// Basic 认证
+        if (option.AuthType == ElasticSearchAuthTypeEnum.Basic) // Basic 认证
         {
-            connectionSettings.BasicAuthentication(option.User, option.Password);
+            connectionSettings.Authentication(new BasicAuthentication(option.User, option.Password));
         }
-        else if (option.AuthType == ElasticSearchAuthTypeEnum.ApiKey) //ApiKey 认证
+        else if (option.AuthType == ElasticSearchAuthTypeEnum.ApiKey) // ApiKey 认证
         {
-            connectionSettings.ApiKeyAuthentication(option.ApiId, option.ApiKey);
+            connectionSettings.Authentication(new ApiKey(option.ApiKey));
         }
-        else if (option.AuthType == ElasticSearchAuthTypeEnum.Base64ApiKey)// Base64ApiKey 认证
+        else if (option.AuthType == ElasticSearchAuthTypeEnum.Base64ApiKey) // Base64ApiKey 认证
         {
-            connectionSettings.ApiKeyAuthentication(new ApiKeyAuthenticationCredentials(option.Base64ApiKey));
+            connectionSettings.Authentication(new Base64ApiKey(option.Base64ApiKey));
         }
         else return;
 
@@ -49,9 +49,7 @@ public static class ElasticSearchSetup
             connectionSettings.CertificateFingerprint(option.Fingerprint);
         }
 
-        var client = new ElasticClient(connectionSettings);
-        client.Indices.Create(option.DefaultIndex, u => u.Map<SysLogOp>(m => m.AutoMap()));
-
+        var client = new ElasticsearchClient(connectionSettings);
         services.AddSingleton(client); // 单例注册
     }
 }

+ 3 - 2
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -39,7 +39,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310000000118, Pid=1310000000111, Title="重置密码", Permission="sysUser:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000119, Pid=1310000000111, Title="设置状态", Permission="sysUser:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000120, Pid=1310000000111, Title="强制下线", Permission="sysOnlineUser:forceOffline", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
-            new SysMenu{ Id=1310000000121, Pid=1310000000111, Title="解除登录锁定", Permission="sysUser:unlockLogin", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000000121, Pid=1310000000111, Title="解除锁定", Permission="sysUser:unlockLogin", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
             new SysMenu{ Id=1310000000131, Pid=1310000000101, Title="角色管理", Path="/system/role", Name="sysRole", Component="/system/role/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
             new SysMenu{ Id=1310000000132, Pid=1310000000131, Title="查询", Permission="sysRole:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
@@ -88,6 +88,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310000000194, Pid=1310000000191, Title="编辑", Permission="sysLdap:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 },
             new SysMenu{ Id=1310000000195, Pid=1310000000191, Title="增加", Permission="sysLdap:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 },
             new SysMenu{ Id=1310000000196, Pid=1310000000191, Title="删除", Permission="sysLdap:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 },
+            new SysMenu{ Id=1310000000197, Pid=1310000000191, Title="同步域账户", Permission="sysLdap:syncUser", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 },
 
             new SysMenu{ Id=1310000000301, Pid=0, Title="平台管理", Path="/platform", Name="platform", Component="Layout", Icon="ele-Menu", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=11000 },
 
@@ -157,7 +158,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310000000414, Pid=1310000000411, Title="增加", Permission="sysPlugin:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000415, Pid=1310000000411, Title="删除", Permission="sysPlugin:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
-            new SysMenu{ Id=1310000000421, Pid=1310000000301, Title="开放接口身份", Path="/platform/openAccess", Name="sysOpenAccess", Component="/system/openAccess/index", Icon="ele-Link", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 },
+            new SysMenu{ Id=1310000000421, Pid=1310000000301, Title="开放接口", Path="/platform/openAccess", Name="sysOpenAccess", Component="/system/openAccess/index", Icon="ele-Link", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 },
             new SysMenu{ Id=1310000000422, Pid=1310000000421, Title="查询", Permission="sysOpenAccess:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000423, Pid=1310000000421, Title="编辑", Permission="sysOpenAccess:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000424, Pid=1310000000421, Title="增加", Permission="sysOpenAccess:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/APIJSON/APIJSONService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// APIJSON服务 💥
+/// APIJSON服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 100)]
 public class APIJSONService : IDynamicApiController, ITransient

+ 5 - 1
Admin.NET/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统域登录信息配置表分页查询输入参数
+/// 系统域登录信息配置输入参数
 /// </summary>
 public class SysLdapInput : BasePageInput
 {
@@ -38,4 +38,8 @@ public class DeleteSysLdapInput : BaseIdInput
 
 public class DetailSysLdapInput : BaseIdInput
 {
+}
+
+public class SyncSysLdapInput : BaseIdInput
+{
 }

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs

@@ -12,7 +12,7 @@ using Lazy.Captcha.Core;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统登录授权服务 💥
+/// 系统登录授权服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 500)]
 public class SysAuthService : IDynamicApiController, ITransient
@@ -102,7 +102,7 @@ public class SysAuthService : IDynamicApiController, ITransient
             {
                 VerifyPassword(input, keyErrorPasswordCount, errorPasswordCount, user);
             }
-            else if (!await _sysLdapService.Auth(tenant.Id, userLdap.Account, input.Password))
+            else if (!await _sysLdapService.AuthAccount(tenant.Id, userLdap.Account, input.Password))
             {
                 _sysCacheService.Set(keyErrorPasswordCount, ++errorPasswordCount, TimeSpan.FromMinutes(30));
                 throw Oops.Oh(ErrorCodeEnum.D1000);

+ 142 - 41
Admin.NET/Admin.NET.Core/Service/Auth/SysLdapService.cs

@@ -11,40 +11,40 @@ using Novell.Directory.Ldap;
 namespace Admin.NET.Core;
 
 /// <summary>
-/// 系统域登录信息配置服务
+/// 系统域登录配置服务 🧩
 /// </summary>
-[ApiDescriptionSettings(Order = 100)]
+[ApiDescriptionSettings(Order = 485)]
 public class SysLdapService : IDynamicApiController, ITransient
 {
     private readonly SqlSugarRepository<SysLdap> _sysLdapRep;
-    private readonly SqlSugarRepository<SysUserLdap> _sysUserLdapRep;
 
-    public SysLdapService(SqlSugarRepository<SysLdap> rep, SqlSugarRepository<SysUserLdap> sysUserLdapRep)
+    public SysLdapService(SqlSugarRepository<SysLdap> sysLdapRep)
     {
-        _sysLdapRep = rep;
-        _sysUserLdapRep = sysUserLdapRep;
+        _sysLdapRep = sysLdapRep;
     }
 
     /// <summary>
-    /// 获取系统域登录信息配置分页列表
+    /// 获取系统域登录配置分页列表 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
+    [DisplayName("获取系统域登录配置分页列表")]
     public async Task<SqlSugarPagedList<SysLdap>> Page(SysLdapInput input)
     {
         return await _sysLdapRep.AsQueryable()
-             .WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), u => u.Host.Contains(input.SearchKey.Trim()))
-             .WhereIF(!string.IsNullOrWhiteSpace(input.Host), u => u.Host.Contains(input.Host.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), u => u.Host.Contains(input.SearchKey.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Host), u => u.Host.Contains(input.Host.Trim()))
             .OrderBy(u => u.CreateTime, OrderByType.Desc)
             .ToPagedListAsync(input.Page, input.PageSize);
     }
 
     /// <summary>
-    /// 增加系统域登录信息配置
+    /// 增加系统域登录配置 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
     [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    [DisplayName("增加系统域登录配置")]
     public async Task<long> Add(AddSysLdapInput input)
     {
         var entity = input.Adapt<SysLdap>();
@@ -54,11 +54,12 @@ public class SysLdapService : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 更新系统域登录信息配置
+    /// 更新系统域登录配置 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
     [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    [DisplayName("更新系统域登录配置")]
     public async Task Update(UpdateSysLdapInput input)
     {
         var entity = input.Adapt<SysLdap>();
@@ -66,94 +67,194 @@ public class SysLdapService : IDynamicApiController, ITransient
         {
             entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); // 加密
         }
+
         await _sysLdapRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
     }
 
     /// <summary>
-    /// 删除系统域登录信息配置
+    /// 删除系统域登录配置 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
     [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    [DisplayName("删除系统域登录配置")]
     public async Task Delete(DeleteSysLdapInput input)
     {
         var entity = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
-        await _sysLdapRep.FakeDeleteAsync(entity);  // 假删除
-        //await _rep.DeleteAsync(entity);  // 真删除
+        await _sysLdapRep.FakeDeleteAsync(entity); // 假删除
+        //await _rep.DeleteAsync(entity); // 真删除
     }
 
     /// <summary>
-    /// 获取系统域登录信息配置详情
+    /// 获取系统域登录配置详情 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [ApiDescriptionSettings(Name = "Detail")]
+    [DisplayName("获取系统域登录配置详情")]
     public async Task<SysLdap> GetDetail([FromQuery] DetailSysLdapInput input)
     {
         return await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id);
     }
 
     /// <summary>
-    /// 获取系统域登录信息配置列表
+    /// 获取系统域登录配置列表 🔖
     /// </summary>
-    /// <param name="input"></param>
     /// <returns></returns>
-    [ApiDescriptionSettings(Name = "List")]
-    public async Task<List<SysLdap>> GetList([FromQuery] SysLdapInput input)
+    [DisplayName("获取系统域登录配置列表")]
+    public async Task<List<SysLdap>> GetList()
     {
         return await _sysLdapRep.AsQueryable().Select<SysLdap>().ToListAsync();
     }
 
     /// <summary>
-    /// 账号验证
+    /// 验证账号
     /// </summary>
     /// <param name="account">域用户</param>
     /// <param name="password">密码</param>
     /// <param name="tenantId">租户</param>
     /// <returns></returns>
     [NonAction]
-    public async Task<bool> Auth(long tenantId, string account, string password)
+    public async Task<bool> AuthAccount(long tenantId, string account, string password)
     {
-        var ldap = await _sysLdapRep.GetFirstAsync(u => u.TenantId == tenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.TenantId == tenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
         var ldapConn = new LdapConnection();
         try
         {
-            ldapConn.Connect(ldap.Host, ldap.Port);
-            ldapConn.Bind(ldap.Version, ldap.BindDn, ldap.BindPass);
-            var userEntitys = ldapConn.Search(ldap.BaseDn, LdapConnection.ScopeSub, $"{ldap.AuthFilter}={account}", null, false);
+            ldapConn.Connect(sysLdap.Host, sysLdap.Port);
+            ldapConn.Bind(sysLdap.Version, sysLdap.BindDn, sysLdap.BindPass);
+            var ldapSearchResults = ldapConn.Search(sysLdap.BaseDn, LdapConnection.ScopeSub, sysLdap.AuthFilter.Replace("$s", account), null, false);
             string dn = string.Empty;
-            while (userEntitys.HasMore())
+            while (ldapSearchResults.HasMore())
             {
-                var entity = userEntitys.Next();
-                var sAMAccountName = entity.GetAttribute(ldap.AuthFilter)?.StringValue;
+                var ldapEntry = ldapSearchResults.Next();
+                var sAMAccountName = ldapEntry.GetAttribute(sysLdap.AuthFilter)?.StringValue;
                 if (!string.IsNullOrEmpty(sAMAccountName))
                 {
-                    dn = entity.Dn;
+                    dn = ldapEntry.Dn;
                     break;
                 }
             }
+
             if (string.IsNullOrEmpty(dn)) throw Oops.Oh(ErrorCodeEnum.D1002);
-            var attr = new LdapAttribute("userPassword", password);
+            // var attr = new LdapAttribute("userPassword", password);
             ldapConn.Bind(dn, password);
         }
         catch (LdapException e)
         {
-            switch (e.ResultCode)
+            return e.ResultCode switch
             {
-                case LdapException.NoSuchObject:
-                case LdapException.NoSuchAttribute:
-                    throw Oops.Oh(ErrorCodeEnum.D0009);
-                case LdapException.InvalidCredentials:
-                    return false;
-
-                default:
-                    throw Oops.Oh(e.Message);
-            }
+                LdapException.NoSuchObject or LdapException.NoSuchAttribute => throw Oops.Oh(ErrorCodeEnum.D0009),
+                LdapException.InvalidCredentials => false,
+                _ => throw Oops.Oh(e.Message),
+            };
         }
         finally
         {
             ldapConn.Disconnect();
         }
+
         return true;
     }
+
+    /// <summary>
+    /// 同步域用户 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("同步域用户")]
+    public async Task SyncUser(SyncSysLdapInput input)
+    {
+        var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        var ldapConn = new LdapConnection();
+        try
+        {
+            ldapConn.Connect(sysLdap.Host, sysLdap.Port);
+            ldapConn.Bind(sysLdap.Version, sysLdap.BindDn, sysLdap.BindPass);
+            var ldapSearchResults = ldapConn.Search(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
+            var userLdapList = new List<SysUserLdap>();
+            while (ldapSearchResults.HasMore())
+            {
+                LdapEntry ldapEntry;
+                try
+                {
+                    ldapEntry = ldapSearchResults.Next();
+                    if (ldapEntry == null) continue;
+                }
+                catch (LdapException)
+                {
+                    continue;
+                }
+
+                var attrs = ldapEntry.GetAttributeSet();
+                if (attrs.Count == 0 || attrs.ContainsKey("OU"))
+                    SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn);
+                else
+                {
+                    var sysUserLdap = new SysUserLdap
+                    {
+                        Account = !attrs.ContainsKey(sysLdap.BindAttrAccount) ? null : attrs.GetAttribute(sysLdap.BindAttrAccount)?.StringValue,
+                        EmployeeId = !attrs.ContainsKey(sysLdap.BindAttrEmployeeId) ? null : attrs.GetAttribute(sysLdap.BindAttrEmployeeId)?.StringValue
+                    };
+                    if (string.IsNullOrEmpty(sysUserLdap.EmployeeId)) continue;
+                    userLdapList.Add(sysUserLdap);
+                }
+            }
+
+            if (userLdapList.Count == 0)
+                return;
+
+            await App.GetRequiredService<SysUserLdapService>().InsertUserLdaps(sysLdap.TenantId!.Value, userLdapList);
+        }
+        catch (LdapException e)
+        {
+            throw e.ResultCode switch
+            {
+                LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009),
+                _ => Oops.Oh(e.Message),
+            };
+        }
+        finally
+        {
+            ldapConn.Disconnect();
+        }
+    }
+
+    /// <summary>
+    /// 遍历查询域用户
+    /// </summary>
+    /// <param name="conn"></param>
+    /// <param name="ldap"></param>
+    /// <param name="userLdapList"></param>
+    /// <param name="baseDn"></param>
+    private static void SearchDnLdapUser(LdapConnection conn, SysLdap ldap, List<SysUserLdap> userLdapList, string baseDn)
+    {
+        var ldapSearchResults = conn.Search(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
+        while (ldapSearchResults.HasMore())
+        {
+            LdapEntry ldapEntry;
+            try
+            {
+                ldapEntry = ldapSearchResults.Next();
+                if (ldapEntry == null) continue;
+            }
+            catch (LdapException)
+            {
+                continue;
+            }
+
+            var attrs = ldapEntry.GetAttributeSet();
+            if (attrs.Count == 0 || attrs.ContainsKey("OU"))
+                SearchDnLdapUser(conn, ldap, userLdapList, ldapEntry.Dn);
+            else
+            {
+                var sysUserLdap = new SysUserLdap
+                {
+                    Account = !attrs.ContainsKey(ldap.BindAttrAccount) ? null : attrs.GetAttribute(ldap.BindAttrAccount)?.StringValue,
+                    EmployeeId = !attrs.ContainsKey(ldap.BindAttrEmployeeId) ? null : attrs.GetAttribute(ldap.BindAttrEmployeeId)?.StringValue
+                };
+                if (string.IsNullOrEmpty(sysUserLdap.EmployeeId)) continue;
+                userLdapList.Add(sysUserLdap);
+            }
+        }
+    }
 }

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs

@@ -11,7 +11,7 @@ using NewLife.Caching.Models;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统缓存服务 💥
+/// 系统缓存服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 400)]
 public class SysCacheService : IDynamicApiController, ISingleton

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统代码生成配置服务 💥
+/// 系统代码生成配置服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 260)]
 public class SysCodeGenConfigService : IDynamicApiController, ITransient
@@ -143,7 +143,7 @@ public class SysCodeGenConfigService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="codeGenConfig"></param>
     /// <returns></returns>
-    private string GetDefaultQueryType(SysCodeGenConfig codeGenConfig)
+    private static string GetDefaultQueryType(SysCodeGenConfig codeGenConfig)
     {
         return (codeGenConfig.NetType?.TrimEnd('?')) switch
         {

+ 9 - 7
Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs

@@ -11,7 +11,7 @@ using System.IO.Compression;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统代码生成器服务 💥
+/// 系统代码生成器服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 270)]
 public class SysCodeGenService : IDynamicApiController, ITransient
@@ -220,18 +220,20 @@ public class SysCodeGenService : IDynamicApiController, ITransient
         for (int i = result.Count - 1; i >= 0; i--)
         {
             var columnOutput = result[i];
-            // 先找自定义字段名的
-            var propertyInfo = entityProperties.FirstOrDefault(p => (p.GetCustomAttribute<SugarColumn>()?.ColumnName ?? "").ToLower() == columnOutput.ColumnName.ToLower());
-            // 如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
-            if (propertyInfo == null)
-                propertyInfo = entityProperties.FirstOrDefault(p => p.GetCustomAttribute<SugarColumn>() != null && p.Name.ToLower() == (config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower() : columnOutput.ColumnName.ToLower()));
+            // 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
+            var propertyInfo = entityProperties.FirstOrDefault(p => (p.GetCustomAttribute<SugarColumn>()?.ColumnName ?? "").ToLower() == columnOutput.ColumnName.ToLower()) ??
+                entityProperties.FirstOrDefault(p => p.GetCustomAttribute<SugarColumn>() != null && p.Name.ToLower() == (config.DbSettings.EnableUnderLine
+                ? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower()
+                : columnOutput.ColumnName.ToLower()));
             if (propertyInfo != null)
             {
                 columnOutput.PropertyName = propertyInfo.Name;
                 columnOutput.ColumnComment = propertyInfo.GetCustomAttribute<SugarColumn>().ColumnDescription;
             }
             else
-                result.RemoveAt(i); //移除没有定义此属性的字段
+            {
+                result.RemoveAt(i); // 移除没有定义此属性的字段
+            }
         }
         return result;
     }

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs

@@ -14,7 +14,7 @@ using Swashbuckle.AspNetCore.SwaggerGen;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统通用服务 💥
+/// 系统通用服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 101)]
 [AllowAnonymous]

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统参数配置服务 💥
+/// 系统参数配置服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 440)]
 public class SysConfigService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Const/SysConstService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统常量服务 💥
+/// 系统常量服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 280)]
 [AllowAnonymous]

+ 58 - 0
Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs

@@ -0,0 +1,58 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 库表可视化
+/// </summary>
+public class VisualDbTable
+{
+    public List<VisualTable> VisualTableList { get; set; }
+
+    public List<VisualColumn> VisualColumnList { get; set; }
+
+    public List<ColumnRelation> ColumnRelationList { get; set; }
+}
+
+public class VisualTable
+{
+    public string TableName { get; set; }
+
+    public string TableComents { get; set; }
+
+    public int X { get; set; }
+
+    public int Y { get; set; }
+}
+
+public class VisualColumn
+{
+    public string TableName { get; set; }
+
+    public string ColumnName { get; set; }
+
+    public string DataType { get; set; }
+
+    public string DataLength { get; set; }
+
+    public string ColumnDescription { get; set; }
+}
+
+public class ColumnRelation
+{
+    public string SourceTableName { get; set; }
+
+    public string SourceColumnName { get; set; }
+
+    public string Type { get; set; }
+
+    public string TargetTableName { get; set; }
+
+    public string TargetColumnName { get; set; }
+}

+ 61 - 1
Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs

@@ -13,7 +13,7 @@ using Npgsql;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统数据库管理服务 💥
+/// 系统数据库管理服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 250)]
 public class SysDatabaseService : IDynamicApiController, ITransient
@@ -41,6 +41,66 @@ public class SysDatabaseService : IDynamicApiController, ITransient
         return App.GetOptions<DbConnectionOptions>().ConnectionConfigs.Select(u => u.ConfigId.ToString()).ToList();
     }
 
+    /// <summary>
+    /// 获取可视化库表结构 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取可视化库表结构")]
+    public VisualDbTable GetVisualDbTable()
+    {
+        var visualTableList = new List<VisualTable>();
+        var visualColumnList = new List<VisualColumn>();
+        var columnRelationList = new List<ColumnRelation>();
+
+        // 遍历所有实体获取所有库表结构
+        var random = new Random();
+        var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)).ToList();
+        foreach (var entityType in entityTypes)
+        {
+            var entityInfo = _db.EntityMaintenance.GetEntityInfoNoCache(entityType);
+
+            var visualTable = new VisualTable
+            {
+                TableName = entityInfo.DbTableName,
+                TableComents = entityInfo.TableDescription + entityInfo.DbTableName,
+                X = random.Next(5000),
+                Y = random.Next(5000)
+            };
+            visualTableList.Add(visualTable);
+
+            foreach (EntityColumnInfo columnInfo in entityInfo.Columns)
+            {
+                var visualColumn = new VisualColumn
+                {
+                    TableName = columnInfo.DbTableName,
+                    ColumnName = columnInfo.DbColumnName,
+                    DataType = columnInfo.PropertyInfo.PropertyType.Name,
+                    DataLength = columnInfo.Length.ToString(),
+                    ColumnDescription = columnInfo.ColumnDescription,
+                };
+                visualColumnList.Add(visualColumn);
+
+                // 根据导航配置获取表之间关联关系
+                if (columnInfo.Navigat != null)
+                {
+                    var name1 = columnInfo.Navigat.GetName();
+                    var name2 = columnInfo.Navigat.GetName2();
+                    var relation = new ColumnRelation
+                    {
+                        SourceTableName = columnInfo.DbTableName,
+                        SourceColumnName = name1,
+                        Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY",
+                        TargetTableName = columnInfo.DbColumnName,
+                        TargetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2
+                    };
+                    columnRelationList.Add(relation);
+                }
+            }
+        }
+
+        return new VisualDbTable { VisualTableList = visualTableList, VisualColumnList = visualColumnList, ColumnRelationList = columnRelationList };
+    }
+
     /// <summary>
     /// 获取字段列表 🔖
     /// </summary>

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统字典值服务 💥
+/// 系统字典值服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 420)]
 [AllowAnonymous]

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统字典类型服务 💥
+/// 系统字典类型服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 430)]
 [AllowAnonymous]

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统枚举服务 💥
+/// 系统枚举服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 275)]
 [AllowAnonymous]

+ 9 - 5
Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs

@@ -13,7 +13,7 @@ using OnceMi.AspNetCore.OSS;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统文件服务 💥
+/// 系统文件服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 410)]
 public class SysFileService : IDynamicApiController, ITransient
@@ -23,6 +23,7 @@ public class SysFileService : IDynamicApiController, ITransient
     private readonly OSSProviderOptions _OSSProviderOptions;
     private readonly UploadOptions _uploadOptions;
     private readonly IOSSService _OSSService;
+    private readonly string _imageType = ".jpg.png.bmp.gif.tif";
 
     public SysFileService(UserManager userManager,
         SqlSugarRepository<SysFile> sysFileRep,
@@ -241,7 +242,7 @@ public class SysFileService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="file">文件</param>
     /// <param name="savePath">路径</param>
-    /// <param name="allowSuffix">允许的格式,比如 .jpg.png.gif.tif.bmp </param>
+    /// <param name="allowSuffix">允许格式:.jpg.png.gif.tif.bmp</param>
     /// <returns></returns>
     private async Task<SysFile> HandleUploadFile(IFormFile file, string savePath, string allowSuffix = "")
     {
@@ -278,12 +279,15 @@ public class SysFileService : IDynamicApiController, ITransient
             });
         }
 
+        // 验证文件类型
         if (!_uploadOptions.ContentType.Contains(file.ContentType))
             throw Oops.Oh(ErrorCodeEnum.D8001);
 
+        // 验证文件大小
         if (sizeKb > _uploadOptions.MaxSize)
             throw Oops.Oh(ErrorCodeEnum.D8002);
 
+        // 获取文件后缀
         var suffix = Path.GetExtension(file.FileName).ToLower(); // 后缀
         if (string.IsNullOrWhiteSpace(suffix))
         {
@@ -296,7 +300,7 @@ public class SysFileService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(suffix))
             throw Oops.Oh(ErrorCodeEnum.D8003);
 
-        //增强安全,防止客户端伪造文件
+        // 防止客户端伪造文件类型
         if (!string.IsNullOrWhiteSpace(allowSuffix) && !allowSuffix.Contains(suffix))
             throw Oops.Oh(ErrorCodeEnum.D8003);
         if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix))
@@ -391,7 +395,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [DisplayName("上传头像")]
     public async Task<SysFile> UploadAvatar([Required] IFormFile file)
     {
-        var sysFile = await HandleUploadFile(file, "Upload/Avatar", ".jpg.png.gif.tif.bmp");
+        var sysFile = await HandleUploadFile(file, "Upload/Avatar", _imageType);
 
         var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
         var user = sysUserRep.GetFirst(u => u.Id == _userManager.UserId);
@@ -413,7 +417,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [DisplayName("上传电子签名")]
     public async Task<SysFile> UploadSignature([Required] IFormFile file)
     {
-        var sysFile = await HandleUploadFile(file, "Upload/Signature", ".jpg.png.gif.tif.bmp");
+        var sysFile = await HandleUploadFile(file, "Upload/Signature", _imageType);
 
         var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
         var user = sysUserRep.GetFirst(u => u.Id == _userManager.UserId);

+ 8 - 8
Admin.NET/Admin.NET.Core/Service/Job/JobClusterServer.cs

@@ -25,7 +25,7 @@ public class JobClusterServer : IJobClusterServer
     /// <param name="context">作业集群服务上下文</param>
     public async void Start(JobClusterContext context)
     {
-        var _sysJobClusterRep = App.GetService<SqlSugarRepository<SysJobCluster>>();
+        var _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
         // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting
         if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId))
         {
@@ -53,11 +53,11 @@ public class JobClusterServer : IJobClusterServer
 
             try
             {
-                ICache _cache = App.GetService<ICache>();
-                //使用分布式锁
+                ICache _cache = App.GetRequiredService<ICache>();
+                // 使用分布式锁
                 using (_cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000))
                 {
-                    var _sysJobClusterRep = App.GetService<SqlSugarRepository<SysJobCluster>>();
+                    var _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
                     // 在这里查询数据库,根据以下两种情况处理
                     // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环
                     // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;
@@ -79,7 +79,7 @@ public class JobClusterServer : IJobClusterServer
     /// <param name="context">作业集群服务上下文</param>
     public async void Stop(JobClusterContext context)
     {
-        var _sysJobClusterRep = App.GetService<SqlSugarRepository<SysJobCluster>>();
+        var _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
         // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
         await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
     }
@@ -90,7 +90,7 @@ public class JobClusterServer : IJobClusterServer
     /// <param name="context">作业集群服务上下文</param>
     public async void Crash(JobClusterContext context)
     {
-        var _sysJobClusterRep = App.GetService<SqlSugarRepository<SysJobCluster>>();
+        var _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
         // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
         await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
     }
@@ -100,9 +100,9 @@ public class JobClusterServer : IJobClusterServer
     /// </summary>
     /// <param name="clusterId">集群 Id</param>
     /// <returns></returns>
-    private async Task WorkNowAsync(string clusterId)
+    private static async Task WorkNowAsync(string clusterId)
     {
-        var _sysJobClusterRep = App.GetService<SqlSugarRepository<SysJobCluster>>();
+        var _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
         // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working
         await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId);
     }

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统作业任务服务 💥
+/// 系统作业任务服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 320)]
 public class SysJobService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Logging/SysLogDiffService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统差异日志服务 💥
+/// 系统差异日志服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 330)]
 public class SysLogDiffService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Logging/SysLogExService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统异常日志服务 💥
+/// 系统异常日志服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 350)]
 public class SysLogExService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Logging/SysLogOpService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统操作日志服务 💥
+/// 系统操作日志服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 360)]
 public class SysLogOpService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Logging/SysLogVisService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统访问日志服务 💥
+/// 系统访问日志服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 340)]
 public class SysLogVisService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统菜单服务 💥
+/// 系统菜单服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 450)]
 public class SysMenuService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs

@@ -12,7 +12,7 @@ using MimeKit;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统邮件发送服务 💥
+/// 系统邮件发送服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 370)]
 public class SysEmailService : IDynamicApiController, ITransient

+ 1 - 4
Admin.NET/Admin.NET.Core/Service/Message/SysMessageService.cs

@@ -11,21 +11,18 @@ using Microsoft.AspNetCore.SignalR;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统消息发送服务 💥
+/// 系统消息发送服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 370)]
 public class SysMessageService : IDynamicApiController, ITransient
 {
     private readonly SysCacheService _sysCacheService;
-    private readonly EmailOptions _emailOptions;
     private readonly IHubContext<OnlineUserHub, IOnlineUserHub> _chatHubContext;
 
     public SysMessageService(SysCacheService sysCacheService,
-        IOptions<EmailOptions> emailOptions,
         IHubContext<OnlineUserHub, IOnlineUserHub> chatHubContext)
     {
         _sysCacheService = sysCacheService;
-        _emailOptions = emailOptions.Value;
         _chatHubContext = chatHubContext;
     }
 

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs

@@ -11,7 +11,7 @@ using AlibabaCloud.SDK.Dysmsapi20170525.Models;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统短信服务 💥
+/// 系统短信服务 🧩
 /// </summary>
 [AllowAnonymous]
 [ApiDescriptionSettings(Order = 150)]

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统通知公告服务 💥
+/// 系统通知公告服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 380)]
 public class SysNoticeService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs

@@ -12,7 +12,7 @@ using System.Security.Claims;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统OAuth服务 💥
+/// 系统OAuth服务 🧩
 /// </summary>
 [AllowAnonymous]
 [ApiDescriptionSettings(Order = 495)]

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs

@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.SignalR;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统在线用户服务 💥
+/// 系统在线用户服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 300)]
 public class SysOnlineUserService : IDynamicApiController, ITransient

+ 9 - 9
Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs

@@ -12,7 +12,7 @@ using System.Security.Cryptography;
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 开放接口身份服务 💥
+/// 开放接口身份服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 244)]
 public class SysOpenAccessService : IDynamicApiController, ITransient
@@ -132,9 +132,9 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
     /// </summary>
     /// <returns></returns>
     [DisplayName("创建密钥")]
-    public Task<string> CreateSecret()
+    public async Task<string> CreateSecret()
     {
-        return Task.FromResult(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..^2]);
+        return await Task.FromResult(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..^2]);
     }
 
     /// <summary>
@@ -143,9 +143,9 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
     /// <param name="accessKey"></param>
     /// <returns></returns>
     [NonAction]
-    public Task<SysOpenAccess> GetByKey(string accessKey)
+    public async Task<SysOpenAccess> GetByKey(string accessKey)
     {
-        return Task.FromResult(
+        return await Task.FromResult(
             _sysCacheService.GetOrAdd(CacheConst.KeyOpenAccess + accessKey, _ =>
             {
                 return _sysOpenAccessRep.AsQueryable()
@@ -166,22 +166,22 @@ public class SysOpenAccessService : IDynamicApiController, ITransient
         {
             OnGetAccessSecret = context =>
             {
-                var logger = context.HttpContext.RequestServices.GetService<ILogger<SysOpenAccessService>>();
+                var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<SysOpenAccessService>>();
                 try
                 {
-                    var openAccessService = context.HttpContext.RequestServices.GetService<SysOpenAccessService>();
+                    var openAccessService = context.HttpContext.RequestServices.GetRequiredService<SysOpenAccessService>();
                     var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult();
                     return Task.FromResult(openAccess == null ? "" : openAccess.AccessSecret);
                 }
                 catch (Exception ex)
                 {
-                    logger.LogError(ex, ex.Message);
+                    logger.LogError(ex, "开发接口身份验证");
                     return Task.FromResult("");
                 }
             },
             OnValidated = context =>
             {
-                var openAccessService = context.HttpContext.RequestServices.GetService<SysOpenAccessService>();
+                var openAccessService = context.HttpContext.RequestServices.GetRequiredService<SysOpenAccessService>();
                 var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult();
                 var identity = ((ClaimsIdentity)context.Principal!.Identity!);
 

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统机构服务 💥
+/// 系统机构服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 470)]
 public class SysOrgService : IDynamicApiController, ITransient
@@ -84,7 +84,7 @@ public class SysOrgService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="orgTree"></param>
     /// <param name="userOrgIdList"></param>
-    private void HandlerOrgTree(List<SysOrg> orgTree, List<long> userOrgIdList)
+    private static void HandlerOrgTree(List<SysOrg> orgTree, List<long> userOrgIdList)
     {
         foreach (var org in orgTree)
         {

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统动态插件服务 💥
+/// 系统动态插件服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 245)]
 public class SysPluginService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统职位服务 💥
+/// 系统职位服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 460)]
 public class SysPosService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统打印模板服务 💥
+/// 系统打印模板服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 305)]
 public class SysPrintService : IDynamicApiController, ITransient

+ 3 - 3
Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs

@@ -9,15 +9,15 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统行政区域服务 💥
+/// 系统行政区域服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 310)]
 public class SysRegionService : IDynamicApiController, ITransient
 {
     private readonly SqlSugarRepository<SysRegion> _sysRegionRep;
 
-    // Url地址-国家统计局行政区域2023年
-    private readonly string _url = "http://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/2023/index.html";
+    //// Url地址-国家统计局行政区域2023年
+    //private readonly string _url = "http://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/2023/index.html";
 
     public SysRegionService(SqlSugarRepository<SysRegion> sysRegionRep)
     {

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统角色服务 💥
+/// 系统角色服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 480)]
 public class SysRoleService : IDynamicApiController, ITransient

+ 4 - 4
Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs

@@ -8,16 +8,16 @@
 
 using AngleSharp.Html.Parser;
 using AspNetCoreRateLimit;
+using Elastic.Clients.Elasticsearch;
 using Lazy.Captcha.Core;
 using Magicodes.ExporterAndImporter.Pdf;
 using MailKit.Net.Smtp;
-using Nest;
 using OnceMi.AspNetCore.OSS;
 
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统服务器监控服务 💥
+/// 系统服务器监控服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 290)]
 public class SysServerService : IDynamicApiController, ITransient
@@ -42,7 +42,7 @@ public class SysServerService : IDynamicApiController, ITransient
             SysRunTime = ComputerUtil.GetRunTime(), // 系统运行时间
             RemoteIp = ComputerUtil.GetIpFromOnline(), // 外网地址
             LocalIp = App.HttpContext?.Connection?.LocalIpAddress.ToString(), // 本地地址
-            FrameworkDescription = RuntimeInformation.FrameworkDescription, // NET框架
+            RuntimeInformation.FrameworkDescription, // NET框架
             Environment = App.HostEnvironment.IsDevelopment() ? "Development" : "Production",
             Wwwroot = App.WebHostEnvironment.WebRootPath, // 网站根目录
             Stage = App.HostEnvironment.IsStaging() ? "Stage环境" : "非Stage环境", // 是否Stage环境
@@ -103,7 +103,7 @@ public class SysServerService : IDynamicApiController, ITransient
         var wechatTenpayAssembly = typeof(WechatTenpayClient).Assembly.GetName();
         var ossAssembly = typeof(IOSSServiceFactory).Assembly.GetName();
         var parserAssembly = typeof(Parser).Assembly.GetName();
-        var nestAssembly = typeof(IElasticClient).Assembly.GetName();
+        var nestAssembly = typeof(ElasticsearchClient).Assembly.GetName();
         var limitAssembly = typeof(IpRateLimitMiddleware).Assembly.GetName();
         var htmlParserAssembly = typeof(HtmlParser).Assembly.GetName();
         var fluentEmailAssembly = typeof(SmtpClient).Assembly.GetName();

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统租户管理服务 💥
+/// 系统租户管理服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 390)]
 public class SysTenantService : IDynamicApiController, ITransient

+ 5 - 0
Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs

@@ -62,6 +62,11 @@ public class AddUserInput : SysUser
     [Required(ErrorMessage = "真实姓名不能为空")]
     public override string RealName { get; set; }
 
+    /// <summary>
+    /// 域用户
+    /// </summary>
+    public string DomainAccount { get; set; }
+
     /// <summary>
     /// 角色集合
     /// </summary>

+ 5 - 0
Admin.NET/Admin.NET.Core/Service/User/Dto/UserOutput.cs

@@ -24,4 +24,9 @@ public class UserOutput : SysUser
     /// 角色名称
     /// </summary>
     public string RoleName { get; set; }
+
+    /// <summary>
+    /// 域用户
+    /// </summary>
+    public string DomainAccount { get; set; }
 }

+ 68 - 0
Admin.NET/Admin.NET.Core/Service/User/SysUserLdapService.cs

@@ -0,0 +1,68 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 用户域账号服务
+/// </summary>
+public class SysUserLdapService : ITransient
+{
+    private readonly SqlSugarRepository<SysUserLdap> _sysUserLdapRep;
+
+    public SysUserLdapService(SqlSugarRepository<SysUserLdap> sysUserLdapRep)
+    {
+        _sysUserLdapRep = sysUserLdapRep;
+    }
+
+    /// <summary>
+    /// 批量插入域账号
+    /// </summary>
+    /// <param name="tenantId"></param>
+    /// <param name="sysUserLdaps"></param>
+    /// <returns></returns>
+    public async Task InsertUserLdaps(long tenantId, List<SysUserLdap> sysUserLdaps)
+    {
+        await _sysUserLdapRep.DeleteAsync(u => u.TenantId == tenantId);
+
+        await _sysUserLdapRep.InsertRangeAsync(sysUserLdaps);
+
+        await _sysUserLdapRep.AsUpdateable()
+            .InnerJoin<SysUser>((l, u) => l.EmployeeId == u.Account && u.Status == StatusEnum.Enable && u.IsDelete == false && l.IsDelete == false)
+            .SetColumns((l, u) => new SysUserLdap { UserId = u.Id })
+            .ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 增加域账号
+    /// </summary>
+    /// <param name="tenantId"></param>
+    /// <param name="userId"></param>
+    /// <param name="account"></param>
+    /// <param name="domainAccount"></param>
+    /// <returns></returns>
+    public async Task AddUserLdap(long tenantId, long userId, string account, string domainAccount)
+    {
+        var userLdap = await _sysUserLdapRep.GetFirstAsync(u => u.TenantId == tenantId && u.IsDelete == false && (u.Account == account || u.UserId == userId || u.EmployeeId == domainAccount));
+        if (userLdap != null)
+            await _sysUserLdapRep.DeleteByIdAsync(userLdap.Id);
+
+        if (!string.IsNullOrWhiteSpace(domainAccount))
+            await _sysUserLdapRep.InsertAsync(new SysUserLdap { EmployeeId = account, TenantId = tenantId, UserId = userId, Account = domainAccount });
+    }
+
+    /// <summary>
+    /// 删除域账号
+    /// </summary>
+    /// <param name="userId"></param>
+    /// <returns></returns>
+    public async Task DeleteUserLdapByUserId(long userId)
+    {
+        await _sysUserLdapRep.DeleteAsync(u => u.UserId == userId);
+    }
+}

+ 18 - 4
Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 系统用户服务 💥
+/// 系统用户服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 490)]
 public class SysUserService : IDynamicApiController, ITransient
@@ -22,6 +22,7 @@ public class SysUserService : IDynamicApiController, ITransient
     private readonly SysConfigService _sysConfigService;
     private readonly SysOnlineUserService _sysOnlineUserService;
     private readonly SysCacheService _sysCacheService;
+    private readonly SysUserLdapService _sysUserLdapService;
 
     public SysUserService(UserManager userManager,
         SqlSugarRepository<SysUser> sysUserRep,
@@ -30,7 +31,8 @@ public class SysUserService : IDynamicApiController, ITransient
         SysUserRoleService sysUserRoleService,
         SysConfigService sysConfigService,
         SysOnlineUserService sysOnlineUserService,
-        SysCacheService sysCacheService)
+        SysCacheService sysCacheService,
+        SysUserLdapService sysUserLdapService)
     {
         _userManager = userManager;
         _sysUserRep = sysUserRep;
@@ -40,6 +42,7 @@ public class SysUserService : IDynamicApiController, ITransient
         _sysConfigService = sysConfigService;
         _sysOnlineUserService = sysOnlineUserService;
         _sysCacheService = sysCacheService;
+        _sysUserLdapService = sysUserLdapService;
     }
 
     /// <summary>
@@ -76,7 +79,8 @@ public class SysUserService : IDynamicApiController, ITransient
             {
                 OrgName = a.Name,
                 PosName = b.Name,
-                RoleName = SqlFunc.Subqueryable<SysUserRole>().LeftJoin<SysRole>((m, n) => m.RoleId == n.Id).Where(m => m.UserId == u.Id).SelectStringJoin((m, n) => n.Name, ",")
+                RoleName = SqlFunc.Subqueryable<SysUserRole>().LeftJoin<SysRole>((m, n) => m.RoleId == n.Id).Where(m => m.UserId == u.Id).SelectStringJoin((m, n) => n.Name, ","),
+                DomainAccount = SqlFunc.Subqueryable<SysUserLdap>().Where(m => m.UserId == u.Id).Select(m => m.Account)
             }, true)
             .ToPagedListAsync(input.Page, input.PageSize);
     }
@@ -99,9 +103,14 @@ public class SysUserService : IDynamicApiController, ITransient
         var user = input.Adapt<SysUser>();
         user.Password = CryptogramUtil.Encrypt(password);
         var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync();
+
         input.Id = newUser.Id;
         await UpdateRoleAndExtOrg(input);
 
+        // 增加域账号
+        if (!string.IsNullOrWhiteSpace(input.DomainAccount))
+            await _sysUserLdapService.AddUserLdap(newUser.TenantId.Value, newUser.Id, newUser.Account, input.DomainAccount);
+
         return newUser.Id;
     }
 
@@ -131,6 +140,8 @@ public class SysUserService : IDynamicApiController, ITransient
         var roleIds = await GetOwnRoleList(input.Id);
         if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u)))
             await _sysOnlineUserService.ForceOffline(input.Id);
+        // 更新域账号
+        await _sysUserLdapService.AddUserLdap(user.TenantId.Value, user.Id, user.Account, input.DomainAccount);
     }
 
     /// <summary>
@@ -171,6 +182,9 @@ public class SysUserService : IDynamicApiController, ITransient
 
         // 删除用户扩展机构
         await _sysUserExtOrgService.DeleteUserExtOrgByUserId(input.Id);
+
+        // 删除域账号
+        await _sysUserLdapService.DeleteUserLdapByUserId(input.Id);
     }
 
     /// <summary>
@@ -214,7 +228,7 @@ public class SysUserService : IDynamicApiController, ITransient
             throw Oops.Oh(ErrorCodeEnum.D3005);
 
         // 账号禁用则增加黑名单,账号启用则移除黑名单
-        var sysCacheService = App.GetService<SysCacheService>();
+        var sysCacheService = App.GetRequiredService<SysCacheService>();
         if (input.Status == StatusEnum.Disable)
         {
             sysCacheService.Set($"{CacheConst.KeyBlacklist}{user.Id}", $"{user.RealName}-{user.Phone}");

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs

@@ -22,7 +22,7 @@ public class GenAuthUrlInput
     /// Scope
     /// </summary>
     public string Scope { get; set; }
-    
+
     /// <summary>
     /// State
     /// </summary>

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 微信支付服务 💥
+/// 微信支付服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 210)]
 public class SysWechatPayService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 微信公众号服务 💥
+/// 微信公众号服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 230)]
 public class SysWechatService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 微信账号服务 💥
+/// 微信账号服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 220)]
 public class SysWechatUserService : IDynamicApiController, ITransient

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs

@@ -9,7 +9,7 @@
 namespace Admin.NET.Core.Service;
 
 /// <summary>
-/// 微信小程序服务 💥
+/// 微信小程序服务 🧩
 /// </summary>
 [ApiDescriptionSettings(Order = 240)]
 public class SysWxOpenService : IDynamicApiController, ITransient

+ 28 - 5
Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs

@@ -22,7 +22,7 @@ public static class SqlSugarFilter
     /// <param name="dbConfigId"></param>
     public static void DeleteUserOrgCache(long userId, string dbConfigId)
     {
-        var sysCacheService = App.GetService<SysCacheService>();
+        var sysCacheService = App.GetRequiredService<SysCacheService>();
 
         // 删除用户机构集合缓存
         sysCacheService.Remove($"{CacheConst.KeyUserOrg}{userId}");
@@ -48,8 +48,13 @@ public static class SqlSugarFilter
         var orgFilter = _cache.Get<ConcurrentDictionary<Type, LambdaExpression>>(cacheKey);
         if (orgFilter == null)
         {
-            // 获取用户所属机构
-            var orgIds = App.GetService<SysOrgService>().GetUserOrgIdList().GetAwaiter().GetResult();
+            // 获取用户所属机构,保证同一作用域
+            var orgIds = new List<long>();
+            Scoped.Create((factory, scope) =>
+            {
+                var services = scope.ServiceProvider;
+                orgIds = services.GetService<SysOrgService>().GetUserOrgIdList().GetAwaiter().GetResult();
+            });
             if (orgIds == null || orgIds.Count == 0) return;
 
             // 获取业务实体数据表
@@ -90,7 +95,7 @@ public static class SqlSugarFilter
         if (string.IsNullOrWhiteSpace(userId)) return maxDataScope;
 
         // 获取用户最大数据范围---仅本人数据
-        maxDataScope = App.GetService<SysCacheService>().Get<int>(CacheConst.KeyRoleMaxDataScope + userId);
+        maxDataScope = App.GetRequiredService<SysCacheService>().Get<int>(CacheConst.KeyRoleMaxDataScope + userId);
         if (maxDataScope != (int)DataScopeEnum.Self) return maxDataScope;
 
         // 配置用户数据范围缓存
@@ -186,4 +191,22 @@ public interface IEntityFilter
     /// </summary>
     /// <returns></returns>
     IEnumerable<TableFilterItem<object>> AddEntityFilter();
-}
+}
+
+///// <summary>
+///// 自定义业务实体过滤器示例
+///// </summary>
+//public class TestEntityFilter : IEntityFilter
+//{
+//    public IEnumerable<TableFilterItem<object>> AddEntityFilter()
+//    {
+//        // 构造自定义条件的过滤器
+//        Expression<Func<SysUser, bool>> dynamicExpression = u => u.Remark.Contains("xxx");
+//        var tableFilterItem = new TableFilterItem<object>(typeof(SysUser), dynamicExpression);
+
+//        return new[]
+//        {
+//            tableFilterItem
+//        };
+//    }
+//}

+ 20 - 19
Admin.NET/Admin.NET.Core/Util/ComputerUtil.cs

@@ -16,19 +16,18 @@ public static class ComputerUtil
     /// <returns></returns>
     public static MemoryMetrics GetComputerInfo()
     {
-        MemoryMetricsClient client = new();
         MemoryMetrics memoryMetrics;
         if (IsMacOS())
         {
-            memoryMetrics = client.GetMacOSMetrics();
+            memoryMetrics = MemoryMetricsClient.GetMacOSMetrics();
         }
         else if (IsUnix())
         {
-            memoryMetrics = client.GetUnixMetrics();
+            memoryMetrics = MemoryMetricsClient.GetUnixMetrics();
         }
         else
         {
-            memoryMetrics = client.GetWindowsMetrics();
+            memoryMetrics = MemoryMetricsClient.GetWindowsMetrics();
         }
         memoryMetrics.FreeRam = Math.Round(memoryMetrics.Free / 1024, 2) + "GB";
         memoryMetrics.UsedRam = Math.Round(memoryMetrics.Used / 1024, 2) + "GB";
@@ -319,7 +318,7 @@ public class MemoryMetricsClient
     /// windows系统获取内存信息
     /// </summary>
     /// <returns></returns>
-    public MemoryMetrics GetWindowsMetrics()
+    public static MemoryMetrics GetWindowsMetrics()
     {
         string output = ShellUtil.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value");
         var metrics = new MemoryMetrics();
@@ -340,7 +339,7 @@ public class MemoryMetricsClient
     /// Unix系统获取
     /// </summary>
     /// <returns></returns>
-    public MemoryMetrics GetUnixMetrics()
+    public static MemoryMetrics GetUnixMetrics()
     {
         string output = ShellUtil.Bash("free -m | awk '{print $2,$3,$4,$5,$6}'");
         var metrics = new MemoryMetrics();
@@ -364,7 +363,7 @@ public class MemoryMetricsClient
     /// macOS系统获取
     /// </summary>
     /// <returns></returns>
-    public MemoryMetrics GetMacOSMetrics()
+    public static MemoryMetrics GetMacOSMetrics()
     {
         var metrics = new MemoryMetrics();
         //物理内存大小
@@ -414,13 +413,14 @@ public class ShellUtil
     /// <returns></returns>
     public static string Cmd(string fileName, string args)
     {
-        string output = string.Empty;
-
-        var info = new ProcessStartInfo();
-        info.FileName = fileName;
-        info.Arguments = args;
-        info.RedirectStandardOutput = true;
+        var info = new ProcessStartInfo
+        {
+            FileName = fileName,
+            Arguments = args,
+            RedirectStandardOutput = true
+        };
 
+        var output = string.Empty;
         using (var process = Process.Start(info))
         {
             output = process.StandardOutput.ReadToEnd();
@@ -465,13 +465,14 @@ public class ShellHelper
     /// <returns></returns>
     public static string Cmd(string fileName, string args)
     {
-        string output = string.Empty;
-
-        var info = new ProcessStartInfo();
-        info.FileName = fileName;
-        info.Arguments = args;
-        info.RedirectStandardOutput = true;
+        var info = new ProcessStartInfo
+        {
+            FileName = fileName,
+            Arguments = args,
+            RedirectStandardOutput = true
+        };
 
+        var output = string.Empty;
         using (var process = Process.Start(info))
         {
             output = process.StandardOutput.ReadToEnd();

+ 44 - 38
Admin.NET/Admin.NET.Core/Util/VerifyFileExtensionName.cs

@@ -1,14 +1,20 @@
-using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
-using System.Xml;
-using System.Xml.Linq;
-using System.Xml.Serialization;
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
 namespace Admin.NET.Core;
 
+/// <summary>
+/// 验证文件类型
+/// </summary>
 public static class VerifyFileExtensionName
 {
-    private static IDictionary<string, string> dics_ext = new Dictionary<string, string>();
-    private static IDictionary<string, HashSet<int>> ext_dics = new Dictionary<string, HashSet<int>>();
+    private static readonly IDictionary<string, string> dics_ext = new Dictionary<string, string>();
+    private static readonly IDictionary<string, HashSet<int>> ext_dics = new Dictionary<string, HashSet<int>>();
 
     static VerifyFileExtensionName()
     {
@@ -18,31 +24,31 @@ public static class VerifyFileExtensionName
         dics_ext.Add("49492A00", ".tif");
         dics_ext.Add("424D", ".bmp");
 
-        //PS和CAD
+        // PS和CAD
         dics_ext.Add("38425053", ".psd");
         dics_ext.Add("41433130", ".dwg"); // CAD
         dics_ext.Add("252150532D41646F6265", ".ps");
 
-        //办公文档类
-        dics_ext.Add("D0CF11E0", ".doc"); //ppt、doc、xls
-        dics_ext.Add("504B0304", ".docx");//pptx、docx、xlsx    
-
-        /**注意由于文本文档录入内容过多,则读取文件头时较为多变-START**/
-        dics_ext.Add("0D0A0D0A", ".txt");//txt
-        dics_ext.Add("0D0A2D2D", ".txt");//txt
-        dics_ext.Add("0D0AB4B4", ".txt");//txt        
-        dics_ext.Add("B4B4BDA8", ".txt");//文件头部为汉字
-        dics_ext.Add("73646673", ".txt");//txt,文件头部为英文字母
-        dics_ext.Add("32323232", ".txt");//txt,文件头部内容为数字
-        dics_ext.Add("0D0A09B4", ".txt");//txt,文件头部内容为数字
-        dics_ext.Add("3132330D", ".txt");//txt,文件头部内容为数字      
-        /**注意由于文本文档录入内容过多,则读取文件头时较为多变-END**/
+        // 办公文档类
+        dics_ext.Add("D0CF11E0", ".doc"); // ppt、doc、xls
+        dics_ext.Add("504B0304", ".docx"); // pptx、docx、xlsx
+
+        /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-START */
+        dics_ext.Add("0D0A0D0A", ".txt"); // txt
+        dics_ext.Add("0D0A2D2D", ".txt"); // txt
+        dics_ext.Add("0D0AB4B4", ".txt"); // txt
+        dics_ext.Add("B4B4BDA8", ".txt"); // 文件头部为汉字
+        dics_ext.Add("73646673", ".txt"); // txt,文件头部为英文字母
+        dics_ext.Add("32323232", ".txt"); // txt,文件头部内容为数字
+        dics_ext.Add("0D0A09B4", ".txt"); // txt,文件头部内容为数字
+        dics_ext.Add("3132330D", ".txt"); // txt,文件头部内容为数字
+        /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-END */
 
         dics_ext.Add("7B5C727466", ".rtf"); // 日记本
 
         dics_ext.Add("255044462D312E", ".pdf");
 
-        //视频或音频类
+        // 视频或音频类
         dics_ext.Add("3026B275", ".wma");
         dics_ext.Add("57415645", ".wav");
         dics_ext.Add("41564920", ".avi");
@@ -53,12 +59,12 @@ public static class VerifyFileExtensionName
         dics_ext.Add("6D6F6F76", ".mov");
         dics_ext.Add("3026B2758E66CF11", ".asf");
 
-        //压缩包
+        // 压缩包
         dics_ext.Add("52617221", ".rar");
         dics_ext.Add("504B03040A000000", ".zip");
         dics_ext.Add("1F8B08", ".gz");
 
-        //程序文件
+        // 程序文件
         dics_ext.Add("3C3F786D6C", ".xml");
         dics_ext.Add("68746D6C3E", ".html");
         //dics_ext.Add("7061636B", ".java");
@@ -66,11 +72,11 @@ public static class VerifyFileExtensionName
         //dics_ext.Add("4D5A9000", ".exe");
 
         dics_ext.Add("44656C69766572792D646174653A", ".eml"); // 邮件
-        dics_ext.Add("5374616E64617264204A", ".mdb");//Access数据库文件
+        dics_ext.Add("5374616E64617264204A", ".mdb"); // Access数据库文件
 
         dics_ext.Add("46726F6D", ".mht");
         dics_ext.Add("4D494D45", ".mhtml");
-        //
+
         foreach (var dics in dics_ext)
         {
             if (!ext_dics.ContainsKey(dics.Value))
@@ -79,6 +85,7 @@ public static class VerifyFileExtensionName
                 ext_dics[dics.Value].Add(dics.Key.Length / 2);
         }
     }
+
     /// <summary>
     /// 文件格式和文件内容格式是否一致
     /// </summary>
@@ -89,34 +96,35 @@ public static class VerifyFileExtensionName
     {
         if (stream == null)
             return false;
-        //
+
         suffix = suffix.ToLower();
         if (!ext_dics.ContainsKey(suffix))
             return false;
+
         try
         {
             foreach (var Len in ext_dics[suffix])
             {
                 byte[] b = new byte[Len];
                 stream.Read(b, 0, b.Length);
-                //string fileType = System.Text.Encoding.UTF8.GetString(b);
+                // string fileType = System.Text.Encoding.UTF8.GetString(b);
                 string fileKey = GetFileHeader(b);
                 if (dics_ext.ContainsKey(fileKey))
                     return true;
             }
         }
-        catch (IOException e)
+        catch (IOException)
         {
         }
         return false;
     }
+
     /**
      * 根据文件转换成的字节数组获取文件头信息
-     * 
-     * @param filePath
-     *            文件路径
+     * @param 文件路径
      * @return 文件头信息
      */
+
     private static string GetFileHeader(byte[] b)
     {
         string value = BytesToHexString(b);
@@ -125,7 +133,7 @@ public static class VerifyFileExtensionName
 
     /**
      * 将要读取文件头信息的文件的byte数组转换成string类型表示
-     * 下面这段代码就是用来对文件类型作验证的方法, 
+     * 下面这段代码就是用来对文件类型作验证的方法,
      * 将字节数组的前四位转换成16进制字符串,并且转换的时候,要先和0xFF做一次与运算。
      * 这是因为,整个文件流的字节数组中,有很多是负数,进行了与运算后,可以将前面的符号位都去掉,
      * 这样转换成的16进制字符串最多保留两位,如果是正数又小于10,那么转换后只有一位,
@@ -133,22 +141,20 @@ public static class VerifyFileExtensionName
      * @param src要读取文件头信息的文件的byte数组
      * @return 文件头信息
      */
+
     private static string BytesToHexString(byte[] src)
     {
-        StringBuilder builder = new StringBuilder();
+        var builder = new StringBuilder();
         if (src == null || src.Length <= 0)
-        {
             return null;
-        }
+
         string hv;
         for (int i = 0; i < src.Length; i++)
         {
             // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写
             hv = Convert.ToString(src[i] & 0xFF, 16).ToUpper();
             if (hv.Length < 2)
-            {
                 builder.Append(0);
-            }
             builder.Append(hv);
         }
         return builder.ToString();

+ 5 - 4
Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs

@@ -39,7 +39,7 @@ namespace Admin.NET.Web.Core
             using var serviceScope = _serviceProvider.CreateScope();
 
             // 若当前账号存在黑名单中则授权失败
-            var sysCacheService = serviceScope.ServiceProvider.GetService<SysCacheService>();
+            var sysCacheService = serviceScope.ServiceProvider.GetRequiredService<SysCacheService>();
             if (sysCacheService.ExistKey($"{CacheConst.KeyBlacklist}{context.User.FindFirst(ClaimConst.UserId)?.Value}"))
             {
                 context.Fail();
@@ -47,7 +47,7 @@ namespace Admin.NET.Web.Core
                 return;
             }
 
-            var sysConfigService = serviceScope.ServiceProvider.GetService<SysConfigService>();
+            var sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
             var tokenExpire = await sysConfigService.GetTokenExpire();
             var refreshTokenExpire = await sysConfigService.GetRefreshTokenExpire();
             if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(), tokenExpire, refreshTokenExpire))
@@ -93,10 +93,11 @@ namespace Admin.NET.Web.Core
                 ? httpContext.Request.Path.Value[5..].Replace("/", ":")
                 : httpContext.Request.Path.Value[1..].Replace("/", ":");
 
+            var sysMenuService = App.GetRequiredService<SysMenuService>();
             // 获取用户拥有按钮权限集合
-            var ownBtnPermList = await App.GetService<SysMenuService>().GetOwnBtnPermList();
+            var ownBtnPermList = await sysMenuService.GetOwnBtnPermList();
             // 获取系统所有按钮权限集合
-            var allBtnPermList = await App.GetService<SysMenuService>().GetAllBtnPermList();
+            var allBtnPermList = await sysMenuService.GetAllBtnPermList();
 
             // 已拥有该按钮权限或者所有按钮集合里面不存在
             var exist1 = ownBtnPermList.Exists(u => routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase));

+ 8 - 2
Admin.NET/Admin.NET.Web.Core/Startup.cs

@@ -114,7 +114,7 @@ public class Startup : AppStartup
             //// 替换事件源存储器
             //options.ReplaceStorer(serviceProvider =>
             //{
-            //    var redisCache = serviceProvider.GetService<ICache>();
+            //    var redisCache = serviceProvider.GetRequiredService<ICache>();
             //    // 创建默认内存通道事件源对象,可自定义队列路由key,如:adminnet
             //    return new RedisEventSourceStorer(redisCache, "adminnet", 3000);
             //});
@@ -242,7 +242,13 @@ public class Startup : AppStartup
             }
         });
 
-        app.UseInject(string.Empty);
+        app.UseInject(string.Empty, options =>
+        {
+            foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups())
+            {
+                groupInfo.Description += "<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关!</font></b></u>";
+            }
+        });
 
         app.UseEndpoints(endpoints =>
         {

+ 1 - 1
Admin.NET/Admin.NET.Web.Entry/wwwroot/Template/Service.cs.vm

@@ -223,7 +223,7 @@ if(@column.EffectType == "Upload"){
     @:[ApiDescriptionSettings(Name = "Upload@(@column.PropertyName)"), HttpPost]
     @:public async Task<SysFile> Upload@(@column.PropertyName)([Required] IFormFile file)
     @:{
-            @:var service = App.GetService<SysFileService>();
+            @:var service = App.GetRequiredService<SysFileService>();
             @:return await service.UploadFile(file, "upload/@(@column.PropertyName)"); 
     @:} 
 }

+ 13 - 6
Admin.NET/Admin.NET.Web.Entry/wwwroot/Template/index.vue.vm

@@ -1,6 +1,10 @@
 @{
   var pkField = Model.TableField.Where(c => c.ColumnKey == "True").FirstOrDefault();
-  string pkFieldName = LowerFirstLetter(pkField.PropertyName);
+  string pkFieldName = null;
+  if(pkField != null && !string.IsNullOrEmpty(pkField.PropertyName))
+  {
+    pkFieldName = LowerFirstLetter(pkField.PropertyName);
+  }
   Dictionary<string, int> definedObjects = new Dictionary<string, int>();
   bool haveLikeCdt = false;
   foreach (var column in Model.TableField){
@@ -113,14 +117,17 @@
 				style="width: 100%"
 				v-loading="loading"
 				tooltip-effect="light"
-				row-key="@(@pkFieldName)"
+                @if(@pkFieldName != null)
+                {
+				@:row-key="@(@pkFieldName)"
+                }
                 @@sort-change="sortChange"
 				border="">
         <el-table-column type="index" label="序号" width="55" align="center"/>
         @foreach (var column in Model.TableField){
         if(@column.WhetherTable == "Y"){
         if(@column.EffectType == "Upload"||@column.EffectType == "fk"||@column.EffectType == "ApiTreeSelect"||@column.EffectType == "Switch"||@column.EffectType == "ConstSelector"){
-        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" width="@(column.ColumnComment!=null && column.ColumnComment.Length > 5 ? column.ColumnComment.Length * 15 : 120)" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="">
+        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="">
           @:<template #default="scope">
             if(@column.EffectType == "Upload"){
             @:<el-image
@@ -149,21 +156,21 @@
         </el-table-column>
         }
         else if(@column.EffectType == "Select"){
-          @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" width="@(column.ColumnComment!=null && column.ColumnComment.Length > 5 ? column.ColumnComment.Length * 15 : 140)" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" >
+          @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" >
             @:<template #default="scope">
               @:<el-tag :type="di('@(@column.DictTypeCode)', scope.row.@(@column.LowerPropertyName))?.tagType"> {{di("@(@column.DictTypeCode)", scope.row.@(@column.LowerPropertyName))?.value}} </el-tag>
             @:</template>
           @:</el-table-column>
         }
         else if(@column.EffectType == "EnumSelector"){
-          @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" width="@(column.ColumnComment!=null && column.ColumnComment.Length > 5 ? column.ColumnComment.Length * 15 : 140)" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" >
+          @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" >
             @:<template #default="scope">
               @:<el-tag>{{ getEnumDesc(scope.row.@column.LowerPropertyName, getEnum@(@column.PropertyName)Data_Index)}}</el-tag>
             @:</template>
           @:</el-table-column>
         }
         else {
-        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" width="@(column.ColumnComment!=null && column.ColumnComment.Length > 5 ? column.ColumnComment.Length * 15 : 140)" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" />
+        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" />
         }
         }
         }

+ 7 - 0
Admin.NET/Admin.NET.sln

@@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.GoView", "
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.Elsa", "Plugins\Admin.NET.Plugin.Elsa\Admin.NET.Plugin.Elsa.csproj", "{48EFC3A6-BDC0-4D05-819A-B1FB927FA4C8}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.DingTalk", "Plugins\Admin.NET.Plugin.DingTalk\Admin.NET.Plugin.DingTalk.csproj", "{F6A002AD-CF7F-4771-8597-F12A50A93DAA}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -52,6 +54,10 @@ Global
 		{48EFC3A6-BDC0-4D05-819A-B1FB927FA4C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{48EFC3A6-BDC0-4D05-819A-B1FB927FA4C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{48EFC3A6-BDC0-4D05-819A-B1FB927FA4C8}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -59,6 +65,7 @@ Global
 	GlobalSection(NestedProjects) = preSolution
 		{C4A288D5-0FAA-4F43-9072-B97635D7871D} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 		{48EFC3A6-BDC0-4D05-819A-B1FB927FA4C8} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+		{F6A002AD-CF7F-4771-8597-F12A50A93DAA} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3}

+ 28 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Admin.NET.Plugin.DingTalk.csproj

@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>disable</Nullable>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <NoWarn>1701;1702;1591;8632</NoWarn>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <NoWarn>1701;1702;1591;8632</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="DingTalk.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 36 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Const/DingTalkConst.cs

@@ -0,0 +1,36 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 钉钉相关常量
+/// </summary>
+[Const("钉钉相关常量")]
+public class DingTalkConst
+{
+    /// <summary>
+    /// API分组名称
+    /// </summary>
+    public const string GroupName = "钉钉【DingTalk】";
+
+    /// <summary>
+    /// 姓名
+    /// </summary>
+    public const string NameField = "sys00-name";
+
+    /// <summary>
+    /// 手机号
+    /// </summary>
+    public const string MobileField = "sys00-mobile";
+
+    /// <summary>
+    /// 工号
+    /// </summary>
+    public const string JobNumberField = "sys00-jobNumber";
+}

+ 10 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/DingTalk.json

@@ -0,0 +1,10 @@
+{
+  "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
+
+  "DingTalk": {
+    "AppId": "",
+    "AgentId": "",
+    "ClientId": "xxxx", // 原 AppKey 和 SuiteKey
+    "ClientSecret": "xxxx" // 原 AppSecret 和 SuiteSecret
+  }
+}

+ 78 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkUser.cs

@@ -0,0 +1,78 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 钉钉用户表
+/// </summary>
+[SugarTable(null, "钉钉用户表")]
+public class DingTalkUser : EntityBase
+{
+    /// <summary>
+    /// 系统用户Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "系统用户Id")]
+    public long SysUserId { get; set; }
+
+    /// <summary>
+    /// 系统用户
+    /// </summary>
+    [SugarColumn(IsIgnore = true)]
+    [Navigate(NavigateType.OneToOne, nameof(SysUserId))]
+    [JsonIgnore]
+    public SysUser SysUser { get; set; }
+
+    /// <summary>
+    /// 钉钉用户id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "钉钉用户id", Length = 64)]
+    [Required, MaxLength(64)]
+    public virtual string? DingTalkUserId { get; set; }
+
+    /// <summary>
+    /// UnionId
+    /// </summary>
+    [SugarColumn(ColumnDescription = "UnionId", Length = 64)]
+    [MaxLength(64)]
+    public string? UnionId { get; set; }
+
+    /// <summary>
+    /// 用户名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "用户名", Length = 64)]
+    [MaxLength(64)]
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// 手机号码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "手机号码", Length = 16)]
+    [MaxLength(16)]
+    public string? Mobile { get; set; }
+
+    /// <summary>
+    /// 性别
+    /// </summary>
+    [SugarColumn(ColumnDescription = "性别")]
+    public int? Sex { get; set; }
+
+    /// <summary>
+    /// 头像
+    /// </summary>
+    [SugarColumn(ColumnDescription = "头像", Length = 256)]
+    [MaxLength(256)]
+    public string? Avatar { get; set; }
+
+    /// <summary>
+    /// 工号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "工号", Length = 16)]
+    [MaxLength(16)]
+    public string? JobNumber { get; set; }
+}

+ 28 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Enum/DingTalkConversationTypeEnum.cs

@@ -0,0 +1,28 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 钉钉发送的会话类型枚举
+/// </summary>
+[Description("钉钉发送的会话类型枚举")]
+public enum DingTalkConversationTypeEnum
+{
+    /// <summary>
+    /// 单聊
+    /// </summary>
+    [Description("单聊")]
+    SingleChat = 0,
+
+    /// <summary>
+    /// 群聊
+    /// </summary>
+    [Description("群聊")]
+    GroupChat = 1
+}

+ 24 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/GlobalUsings.cs

@@ -0,0 +1,24 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+global using Admin.NET.Core;
+global using Furion;
+global using Furion.ConfigurableOptions;
+global using Furion.DependencyInjection;
+global using Furion.DynamicApiController;
+global using Furion.FriendlyException;
+global using Furion.RemoteRequest;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Mvc;
+global using Microsoft.Extensions.Options;
+global using Newtonsoft.Json;
+global using SqlSugar;
+global using System.ComponentModel;
+global using System.ComponentModel.DataAnnotations;
+global using System.Data;
+global using System.Linq.Dynamic.Core;

+ 32 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Option/DingTalkOptions.cs

@@ -0,0 +1,32 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public sealed class DingTalkOptions : IConfigurableOptions
+{
+    /// <summary>
+    /// AppId
+    /// </summary>
+    public string AppId { get; set; }
+
+    /// <summary>
+    /// AgentId
+    /// </summary>
+    public string AgentId { get; set; }
+
+    /// <summary>
+    /// 原 AppKey 和 SuiteKey
+    /// </summary>
+    public string ClientId { get; set; }
+
+    /// <summary>
+    /// 原 AppSecret 和 SuiteSecret
+    /// </summary>
+    public string ClientSecret { get; set; }
+}

+ 209 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/DingTalkService.cs

@@ -0,0 +1,209 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk.Service;
+
+/// <summary>
+/// 钉钉服务 🧩
+/// </summary>
+[ApiDescriptionSettings(DingTalkConst.GroupName, Module = "DingTalk", Order = 100)]
+public class DingTalkService : IDynamicApiController, IScoped
+{
+    private readonly IDingTalkApi _dingTalkApi;
+    private readonly DingTalkOptions _dingTalkOptions;
+    private readonly SqlSugarRepository<DingTalkUser> _dingTalkUserRepo;
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
+
+    public DingTalkService(IDingTalkApi dingTalkApi,
+        IOptions<DingTalkOptions> dingTalkOptions,
+        SqlSugarRepository<DingTalkUser> dingTalkUserRepo,
+        SqlSugarRepository<SysUser> sysUserRep)
+    {
+        _dingTalkApi = dingTalkApi;
+        _dingTalkOptions = dingTalkOptions.Value;
+        _dingTalkUserRepo = dingTalkUserRepo;
+        _sysUserRep = sysUserRep;
+    }
+
+    /// <summary>
+    /// 同步钉钉用户 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("同步钉钉用户")]
+    public async Task SyncDingTalkUser()
+    {
+        var param = new GetDingTalkTokenInput()
+        {
+            AppKey = _dingTalkOptions.ClientId,
+            AppSecret = _dingTalkOptions.ClientSecret
+        };
+        var tokenRes = await _dingTalkApi.GetDingTalkToken(param);
+        if (tokenRes.ErrCode != 0)
+            throw Oops.Oh(tokenRes.ErrMsg);
+
+        var offset = 0;
+        while (offset >= 0)
+        {
+            // 获取用户Id列表
+            var userIdsRes = await _dingTalkApi.GetDingTalkCurrentEmployeesList(tokenRes.AccessToken, new GetDingTalkCurrentEmployeesListInput
+            {
+                StatusList = "2,3,5,-1",
+                Size = 50,
+                Offset = offset
+            });
+            if (!userIdsRes.Success)
+                throw Oops.Oh(userIdsRes.ErrMsg);
+
+            // 根据用户Id获取花名册
+            var rosterRes = await _dingTalkApi.GetDingTalkCurrentEmployeesRosterList(tokenRes.AccessToken, new GetDingTalkCurrentEmployeesRosterListInput()
+            {
+                UserIdList = string.Join(",", userIdsRes.Result.DataList),
+                FieldFilterList = $"{DingTalkConst.NameField},{DingTalkConst.JobNumberField},{DingTalkConst.MobileField}",
+                AgentId = _dingTalkOptions.AgentId
+            });
+            if (!rosterRes.Success)
+                throw Oops.Oh(rosterRes.ErrMsg);
+
+            // 判断新增还是更新
+            var userIds = rosterRes.Result.Select(u => u.UserId).ToList();
+            var uDingTalkUser = await _dingTalkUserRepo.AsQueryable()
+                .Where(u => userIds.Contains(u.DingTalkUserId))
+                .ToListAsync();
+
+            var uUserIds = uDingTalkUser.Select(u => u.DingTalkUserId); // 需要更新的用户Id
+            var iUserIds = userIds.Where(u => !uUserIds.Contains(u)); // 需要新增的用户Id
+
+            // 保存钉钉用户
+            var iUsers = rosterRes.Result
+                .Where(u => iUserIds.Contains(u.UserId))
+                .Select(u => new DingTalkUser
+                {
+                    DingTalkUserId = u.UserId,
+                    Name = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.NameField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                    Mobile = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.MobileField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                    JobNumber = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.JobNumberField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                }).ToList();
+            if (iUsers.Count > 0)
+            {
+                await _dingTalkUserRepo.AsInsertable(iUsers).ExecuteCommandAsync();
+            }
+
+            // 更新钉钉用户
+            var uUsers = rosterRes.Result
+                .Where(u => uUserIds.Contains(u.UserId))
+                .Select(u => new DingTalkUser
+                {
+                    Id = uDingTalkUser.Where(m => m.DingTalkUserId == u.UserId).Select(m => m.Id).FirstOrDefault(),
+                    DingTalkUserId = u.UserId,
+                    Name = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.NameField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                    Mobile = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.MobileField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                    JobNumber = u.FieldDataList.Where(m => m.FieldCode == DingTalkConst.JobNumberField).Select(m => m.FieldValueList.Select(n => n.Value).FirstOrDefault()).FirstOrDefault(),
+                }).ToList();
+            if (uUsers.Count > 0)
+            {
+                await _dingTalkUserRepo.AsUpdateable(uUsers).UpdateColumns(u => new
+                {
+                    u.DingTalkUserId,
+                    u.Name,
+                    u.Mobile,
+                    u.JobNumber,
+                    u.UpdateTime,
+                    u.UpdateUserName,
+                    u.UpdateUserId,
+                }).ExecuteCommandAsync();
+            }
+
+            // 保存分页游标
+            if (userIdsRes.Result.NextCursor == null)
+                break;
+            offset = (int)userIdsRes.Result.NextCursor;
+        }
+
+        var sysUser = await _sysUserRep.AsQueryable()
+            .Select(u => new
+            {
+                u.Id,
+                u.Account,
+                u.Phone
+            }).ToListAsync();
+        var dingTalkUser = await _dingTalkUserRepo.AsQueryable()
+            .Where(u => sysUser.Any(m => m.Account == u.JobNumber))
+            .Select(u => new
+            {
+                u.Id,
+                u.JobNumber,
+                u.Mobile
+            }).ToListAsync();
+
+        // 更新钉钉用户中系统用户Id
+        var uDingTalkUsers = dingTalkUser.Select(u => new DingTalkUser
+        {
+            Id = u.Id,
+            SysUserId = sysUser.Where(m => m.Account == u.JobNumber).Select(m => m.Id).FirstOrDefault(),
+        }).ToList();
+        if (uDingTalkUsers.Count > 0)
+        {
+            await _dingTalkUserRepo.AsUpdateable(uDingTalkUsers).UpdateColumns(u => new
+            {
+                u.SysUserId,
+                u.UpdateTime,
+                u.UpdateUserName,
+                u.UpdateUserId,
+            }).ExecuteCommandAsync();
+        }
+
+        return;
+    }
+
+    /// <summary>
+    /// 获取企业内部应用的access_token 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取企业内部应用的access_token")]
+    public async Task<GetDingTalkTokenOutput> GetDingTalkToken([FromQuery] GetDingTalkTokenInput input)
+    {
+        return await _dingTalkApi.GetDingTalkToken(input);
+    }
+
+    /// <summary>
+    /// 获取在职员工列表 🔖
+    /// </summary>
+    /// <param name="access_token"></param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取在职员工列表")]
+    public async Task<DingTalkBaseResponse<GetDingTalkCurrentEmployeesListOutput>> GetDingTalkCurrentEmployeesList(string access_token, [Required] GetDingTalkCurrentEmployeesListInput input)
+    {
+        return await _dingTalkApi.GetDingTalkCurrentEmployeesList(access_token, input);
+    }
+
+    /// <summary>
+    /// 获取员工花名册字段信息 🔖
+    /// </summary>
+    /// <param name="access_token"></param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取员工花名册字段信息")]
+    public async Task<DingTalkBaseResponse<List<DingTalkEmpRosterFieldVo>>> GetDingTalkCurrentEmployeesRosterList(string access_token, [Required] GetDingTalkCurrentEmployeesRosterListInput input)
+    {
+        return await _dingTalkApi.GetDingTalkCurrentEmployeesRosterList(access_token, input);
+    }
+
+    /// <summary>
+    /// 发送钉钉互动卡片 🔖
+    /// </summary>
+    /// <param name="token"></param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("给指定用户发送钉钉互动卡片")]
+    public async Task<DingTalkSendInteractiveCardsOutput> DingTalkSendInteractiveCards(string token, DingTalkSendInteractiveCardsInput input)
+    {
+        return await _dingTalkApi.DingTalkSendInteractiveCards(token, input);
+    }
+}

+ 43 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkBaseResponse.cs

@@ -0,0 +1,43 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 钉钉基础响应结果
+/// </summary>
+/// <typeparam name="T">Data</typeparam>
+public class DingTalkBaseResponse<T>
+{
+    /// <summary>
+    /// 返回结果
+    /// </summary>
+    public T Result { get; set; }
+
+    /// <summary>
+    /// 返回码
+    /// </summary>
+    public int ErrCode { get; set; }
+
+    /// <summary>
+    /// 返回码描述。
+    /// </summary>
+    public string ErrMsg { get; set; }
+
+    /// <summary>
+    /// 是否调用成功
+    /// </summary>
+    public bool Success { get; set; }
+
+    /// <summary>
+    /// 请求Id
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("request_id")]
+    [System.Text.Json.Serialization.JsonPropertyName("request_id")]
+    public string RequestId { get; set; }
+}

+ 38 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCardData.cs

@@ -0,0 +1,38 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 卡片公有数据
+/// </summary>
+public class DingTalkCardData
+{
+    /// <summary>
+    /// 卡片模板内容替换参数,普通文本类型。
+    /// </summary>
+    public DingTalkCardParamMap CardParamMap { get; set; }
+
+    /// <summary>
+    /// 卡片模板内容替换参数,多媒体类型。
+    /// </summary>
+    public string CardMediaIdParamMap { get; set; }
+}
+
+/// <summary>
+/// 卡片模板内容替换参数
+/// </summary>
+public class DingTalkCardParamMap
+{
+    /// <summary>
+    /// 片模板内容替换参数
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("sys_full_json_obj")]
+    [System.Text.Json.Serialization.JsonPropertyName("sys_full_json_obj")]
+    public string SysFullJsonObj { get; set; }
+}

+ 40 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpFieldDataVo.cs

@@ -0,0 +1,40 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class DingTalkEmpFieldDataVo
+{
+    /// <summary>
+    /// 字段名称
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("field_name")]
+    [System.Text.Json.Serialization.JsonPropertyName("field_name")]
+    public string FieldName { get; set; }
+
+    /// <summary>
+    /// 字段标识
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("field_code")]
+    [System.Text.Json.Serialization.JsonPropertyName("field_code")]
+    public string FieldCode { get; set; }
+
+    /// <summary>
+    /// 分组标识
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("group_id")]
+    [System.Text.Json.Serialization.JsonPropertyName("group_id")]
+    public string GroupId { get; set; }
+
+    /// <summary>
+    ///
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("field_value_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("field_value_list")]
+    public List<DingTalkFieldValueVo> FieldValueList { get; set; }
+}

+ 33 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpRosterFieldVo.cs

@@ -0,0 +1,33 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class DingTalkEmpRosterFieldVo
+{
+    /// <summary>
+    /// 企业的corpid
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("corp_id")]
+    [System.Text.Json.Serialization.JsonPropertyName("corp_id")]
+    public string CorpId { get; set; }
+
+    /// <summary>
+    /// 返回的字段信息列表
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("field_data_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("field_data_list")]
+    public List<DingTalkEmpFieldDataVo> FieldDataList { get; set; }
+
+    /// <summary>
+    /// 员工的userid
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("userid")]
+    [System.Text.Json.Serialization.JsonPropertyName("userid")]
+    public string UserId { get; set; }
+}

+ 33 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkFieldValueVo.cs

@@ -0,0 +1,33 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class DingTalkFieldValueVo
+{
+    /// <summary>
+    /// 第几条的明细标识,下标从0开始
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("item_index")]
+    [System.Text.Json.Serialization.JsonPropertyName("item_index")]
+    public int ItemIndex { get; set; }
+
+    /// <summary>
+    /// 字段展示值,选项类型字段对应选项的value
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("label")]
+    [System.Text.Json.Serialization.JsonPropertyName("label")]
+    public string Label { get; set; }
+
+    /// <summary>
+    /// 字段取值,选项类型字段对应选项的key
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("value")]
+    [System.Text.Json.Serialization.JsonPropertyName("value")]
+    public string Value { get; set; }
+}

+ 115 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsInput.cs

@@ -0,0 +1,115 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class DingTalkSendInteractiveCardsInput
+{
+    /// <summary>
+    /// 互动卡片的消息模板Id
+    /// </summary>
+    [Required(ErrorMessage = "互动卡片的消息模板Id必填!")]
+    public string? CardTemplateId { get; set; }
+
+    /// <summary>
+    /// 群Id
+    /// </summary>
+    /// <remarks>
+    /// 1、基于群模板创建的群。
+    /// 企业内部应用,调用创建群接口获取open_conversation_id参数值。
+    /// 2、安装群聊酷应用的群。
+    /// 企业内部应用,通过群内安装酷应用事件获取回调参数OpenConversationId参数值。
+    /// </remarks>
+    public string OpenConversationId { get; set; }
+
+    /// <summary>
+    /// 接收人userId列表
+    /// </summary>
+    /// <remarks>
+    /// 单聊:receiverUserIdList填写用户ID,最大值20。
+    /// 群聊:receiverUserIdList填写用户ID,表示当前对应ID的群内用户可见
+    /// receiverUserIdList参数不填写,表示当前群内所有用户可见
+    /// </remarks>
+    [Required(ErrorMessage = "接收人userId列表必填!")]
+    public List<string>? ReceiverUserIdList { get; set; }
+
+    /// <summary>
+    /// 唯一标示卡片的外部编码
+    /// </summary>
+    [Required(ErrorMessage = "唯一标示卡片的外部编码必填!")]
+    public string? OutTrackId { get; set; }
+
+    /// <summary>
+    /// 机器人的编码
+    /// </summary>
+    public string RobotCode { get; set; }
+
+    /// <summary>
+    /// 发送的会话类型
+    /// </summary>
+    [Required(ErrorMessage = "会话类型必填!")]
+    public DingTalkConversationTypeEnum? ConversationType { get; set; }
+
+    /// <summary>
+    /// 卡片回调时的路由Key,用于查询注册的callbackUrl
+    /// </summary>
+    public string CallbackRouteKey { get; set; }
+
+    /// <summary>
+    /// 卡片公有数据
+    /// </summary>
+    [Required(ErrorMessage = "卡片公有数据必填!")]
+    public DingTalkCardData CardData { get; set; }
+}
+
+public class GetDingTalkCardMessageReadStatusInput
+{
+    /// <summary>
+    /// 机器人的编码
+    /// </summary>
+    public string RobotCode { set; get; }
+
+    /// <summary>
+    /// 消息唯一标识,可通过批量发送人与机器人会话中机器人消息接口返回参数中processQueryKey字段获取。
+    /// </summary>
+    public string ProcessQueryKey { set; get; }
+}
+
+public class GetDingTalkCardMessageReadStatusOutput
+{
+    /// <summary>
+    /// 消息发送状态,SUCCESS:成功、RECALLED:已撤回、PROCESSING: 处理中
+    /// </summary>
+    public string SendStatus { get; set; }
+
+    /// <summary>
+    ///
+    /// </summary>
+    public DingTalkCardMessageReadInfoList MessageReadInfoList { get; set; }
+}
+
+/// <summary>
+/// 钉钉卡片消息已读情况
+/// </summary>
+public class DingTalkCardMessageReadInfoList
+{
+    /// <summary>
+    /// 消息接收者名称
+    /// </summary>
+    public string Name { set; get; }
+
+    /// <summary>
+    /// 消息接收者的userId
+    /// </summary>
+    public string UserId { set; get; }
+
+    /// <summary>
+    /// 已读状态,READ:已读、UNREAD:未读
+    /// </summary>
+    public string ReadStatus { set; get; }
+}

+ 25 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsOutput.cs

@@ -0,0 +1,25 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 发送钉钉互动卡片返回
+/// </summary>
+public class DingTalkSendInteractiveCardsOutput
+{
+    /// <summary>
+    /// 返回结果
+    /// </summary>
+    public bool Success { get; set; }
+
+    /// <summary>
+    /// 创建卡片结果
+    /// </summary>
+    public DingTalkSendInteractiveCardsResult Result { get; set; }
+}

+ 17 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsResult.cs

@@ -0,0 +1,17 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class DingTalkSendInteractiveCardsResult
+{
+    /// <summary>
+    /// 用于业务方后续查看已读列表的查询key
+    /// </summary>
+    public string ProcessQueryKey { get; set; }
+}

+ 32 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListInput.cs

@@ -0,0 +1,32 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+/// <summary>
+/// 获取在职员工列表参数
+/// </summary>
+public class GetDingTalkCurrentEmployeesListInput
+{
+    /// <summary>
+    /// 在职员工状态筛选,可以查询多个状态。不同状态之间使用英文逗号分隔。2:试用期、3:正式、5:待离职、-1:无状态
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("status_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("status_list")]
+    public string StatusList { get; set; }
+
+    /// <summary>
+    /// 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值。
+    /// </summary>
+    public int Offset { get; set; }
+
+    /// <summary>
+    /// 分页大小,最大50。
+    /// </summary>
+    public int Size { get; set; }
+}

+ 26 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListOutput.cs

@@ -0,0 +1,26 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class GetDingTalkCurrentEmployeesListOutput
+{
+    /// <summary>
+    /// 查询到的员工userId列表
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("data_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("data_list")]
+    public List<string> DataList { get; set; }
+
+    /// <summary>
+    /// 下一次分页调用的offset值,当返回结果里没有next_cursor时,表示分页结束。
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("next_cursor")]
+    [System.Text.Json.Serialization.JsonPropertyName("next_cursor")]
+    public int? NextCursor { get; set; }
+}

+ 31 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesRosterListInput.cs

@@ -0,0 +1,31 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class GetDingTalkCurrentEmployeesRosterListInput
+{
+    /// <summary>
+    /// 员工的userId列表,多个userid之间使用逗号分隔,一次最多支持传100个值。
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("userid_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("userid_list")]
+    public string UserIdList { get; set; }
+
+    /// <summary>
+    /// 需要获取的花名册字段field_code值列表,多个字段之间使用逗号分隔,一次最多支持传100个值。
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("field_filter_list")]
+    [System.Text.Json.Serialization.JsonPropertyName("field_filter_list")]
+    public string FieldFilterList { get; set; }
+
+    /// <summary>
+    /// 应用的AgentId
+    /// </summary>
+    public string AgentId { get; set; }
+}

+ 49 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkToken.cs

@@ -0,0 +1,49 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public class GetDingTalkTokenInput
+{
+    /// <summary>
+    /// 应用的唯一标识key
+    /// </summary>
+    public string AppKey { get; set; }
+
+    /// <summary>
+    /// 应用的密钥。AppKey和AppSecret可在钉钉开发者后台的应用详情页面获取。
+    /// </summary>
+    public string AppSecret { get; set; }
+}
+
+public class GetDingTalkTokenOutput
+{
+    /// <summary>
+    /// 生成的access_token
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("access_token")]
+    [System.Text.Json.Serialization.JsonPropertyName("access_token")]
+    public string AccessToken { get; set; }
+
+    /// <summary>
+    /// access_token的过期时间,单位秒
+    /// </summary>
+    [Newtonsoft.Json.JsonProperty("expires_in")]
+    [System.Text.Json.Serialization.JsonPropertyName("expires_in")]
+    public int ExpiresIn { get; set; }
+
+    /// <summary>
+    /// 返回码描述
+    /// </summary>
+    public string ErrMsg { get; set; }
+
+    /// <summary>
+    /// 返回码
+    /// </summary>
+    public int ErrCode { get; set; }
+}

+ 63 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs

@@ -0,0 +1,63 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+using Furion.RemoteRequest;
+
+namespace Admin.NET.Plugin.DingTalk;
+
+public interface IDingTalkApi : IHttpDispatchProxy
+{
+    /// <summary>
+    /// 获取企业内部应用的access_token
+    /// </summary>
+    /// <returns></returns>
+    [Get("https://oapi.dingtalk.com/gettoken")]
+    Task<GetDingTalkTokenOutput> GetDingTalkToken([QueryString] GetDingTalkTokenInput input);
+
+    /// <summary>
+    /// 获取在职员工列表
+    /// </summary>
+    /// <param name="access_token">调用该接口的应用凭证</param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [Post("https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/queryonjob")]
+    Task<DingTalkBaseResponse<GetDingTalkCurrentEmployeesListOutput>> GetDingTalkCurrentEmployeesList([QueryString] string access_token,
+        [Body, Required] GetDingTalkCurrentEmployeesListInput input);
+
+    /// <summary>
+    /// 获取员工花名册字段信息
+    /// </summary>
+    /// <param name="access_token">调用该接口的应用凭证</param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [Post("https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/v2/list")]
+    Task<DingTalkBaseResponse<List<DingTalkEmpRosterFieldVo>>> GetDingTalkCurrentEmployeesRosterList([QueryString] string access_token,
+        [Body, Required] GetDingTalkCurrentEmployeesRosterListInput input);
+
+    /// <summary>
+    /// 发送钉钉互动卡片
+    /// </summary>
+    /// <param name="token">调用该接口的访问凭证</param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [Post("https://api.dingtalk.com/v1.0/im/interactiveCards/send")]
+    Task<DingTalkSendInteractiveCardsOutput> DingTalkSendInteractiveCards(
+        [Headers("x-acs-dingtalk-access-token")] string token,
+        [Body] DingTalkSendInteractiveCardsInput input);
+
+    /// <summary>
+    /// 获取钉钉卡片消息读取状态
+    /// </summary>
+    /// <param name="token"></param>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [Get("https://api.dingtalk.com/v1.0/robot/oToMessages/readStatus")]
+    Task<GetDingTalkCardMessageReadStatusOutput> GetDingTalkCardMessageReadStatus(
+    [Headers("x-acs-dingtalk-access-token")] string token,
+    [QueryString] GetDingTalkCardMessageReadStatusInput input);
+}

+ 26 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs

@@ -0,0 +1,26 @@
+// 大名科技(天津)有限公司 版权所有
+//
+// 此源代码遵循位于源代码树根目录中的 LICENSE 文件的许可证
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动
+//
+// 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Admin.NET.Plugin.DingTalk;
+
+[AppStartup(100)]
+public class Startup : AppStartup
+{
+    public void ConfigureServices(IServiceCollection services)
+    {
+        services.AddConfigurableOptions<DingTalkOptions>();
+    }
+
+    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+    {
+    }
+}

+ 2 - 2
Admin.NET/Plugins/Admin.NET.Plugin.GoView/Const/GoViewConst.cs

@@ -6,7 +6,7 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-namespace Admin.NET.Plugin.GoView.Const;
+namespace Admin.NET.Plugin.GoView;
 
 /// <summary>
 /// GoView 相关常量
@@ -17,5 +17,5 @@ public class GoViewConst
     /// <summary>
     /// API分组名称
     /// </summary>
-    public const string GroupName = "GoView";
+    public const string GroupName = "可视化大屏【GoView";
 }

+ 1 - 1
Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewPro.cs

@@ -6,7 +6,7 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-namespace Admin.NET.Plugin.GoView.Entity;
+namespace Admin.NET.Plugin.GoView;
 
 /// <summary>
 /// GoView 项目表

+ 1 - 1
Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewProData.cs

@@ -6,7 +6,7 @@
 //
 // 任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关
 
-namespace Admin.NET.Plugin.GoView.Entity;
+namespace Admin.NET.Plugin.GoView;
 
 /// <summary>
 /// GoView 项目数据表

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно