Quellcode durchsuchen

Merge branch 'next' of https://gitee.com/ccjungle/Admin.NET into next

ccjungle vor 1 Jahr
Ursprung
Commit
32b65d3aa4
58 geänderte Dateien mit 1186 neuen und 378 gelöschten Zeilen
  1. 4 4
      .gitignore
  2. 2 2
      Admin.NET/Admin.NET.Application/Configuration/Database.json
  3. 15 15
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  4. 25 0
      Admin.NET/Admin.NET.Core/Entity/SysUser.cs
  5. 18 21
      Admin.NET/Admin.NET.Core/Extension/EnumExtension.cs
  6. 2 2
      Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs
  7. 9 8
      Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs
  8. 156 101
      Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs
  9. 4 6
      Admin.NET/Admin.NET.Core/Job/LogJob.cs
  10. 5 5
      Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs
  11. 2 2
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  12. 138 37
      Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs
  13. 1 1
      Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs
  14. 9 0
      Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs
  15. 60 1
      Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs
  16. 3 4
      Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs
  17. 4 3
      Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs
  18. 2 1
      Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs
  19. 2 2
      Admin.NET/Admin.NET.Core/Service/Log/SysLogExService.cs
  20. 2 2
      Admin.NET/Admin.NET.Core/Service/Log/SysLogVisService.cs
  21. 10 13
      Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs
  22. 40 0
      Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs
  23. 4 0
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs
  24. 56 2
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs
  25. 29 1
      Admin.NET/Admin.NET.Web.Core/Startup.cs
  26. BIN
      Admin.NET/Admin.NET.Web.Entry/wwwroot/images/logo.png
  27. 1 1
      Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Service.cs.vm
  28. BIN
      Admin.NET/Admin.NET.Web.Entry/wwwroot/upload/logo.png
  29. 7 0
      Admin.NET/Admin.NET.sln
  30. 23 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Admin.NET.Plugin.K3Cloud.csproj
  31. 20 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Configuration/K3Cloud.json
  32. 8 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/GlobalUsings.cs
  33. 45 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Option/K3CloudOptions.cs
  34. 23 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudBaeInput.cs
  35. 12 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginInput.cs
  36. 62 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginOutput.cs
  37. 66 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudPushResultOutput.cs
  38. 52 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/IK3CloudApi.cs
  39. 31 0
      Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs
  40. 2 1
      Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj
  41. 6 2
      README.md
  42. 67 0
      Web/api_build/build_api.sh
  43. 26 27
      Web/package.json
  44. BIN
      Web/public/favicon.ico
  45. 8 8
      Web/src/api-services/apis/sys-database-api.ts
  46. BIN
      Web/src/assets/logo-mini.svg
  47. BIN
      Web/src/assets/logo.png
  48. 4 1
      Web/src/views/system/codeGen/component/editCodeGenDialog.vue
  49. 6 0
      Web/src/views/system/database/component/genSeedData.vue
  50. 3 8
      Web/src/views/system/menu/component/editMenu.vue
  51. 3 8
      Web/src/views/system/org/component/editOrg.vue
  52. 3 1
      Web/src/views/system/region/component/editRegion.vue
  53. 56 52
      Web/src/views/system/server/index.vue
  54. 6 17
      Web/src/views/system/user/component/editUser.vue
  55. 15 10
      Web/src/views/system/user/component/userCenter.vue
  56. 4 3
      Web/src/views/system/user/index.vue
  57. 24 5
      Web/src/views/system/weChatPay/index.vue
  58. 1 1
      Web/vite.config.ts

+ 4 - 4
.gitignore

@@ -18,10 +18,10 @@ bin-release/
 # information for Eclipse / Flash Builder.
 /Admin.NET/.vs
 /Admin.NET/packages
-/Admin.NET/Admin.NET.Web.Entry/wwwroot/Upload
-/Admin.NET/Admin.NET.Web.Entry/wwwroot/Avatar
-/Admin.NET/Admin.NET.Web.Entry/wwwroot/Signature
-/Admin.NET/Admin.NET.Web.Entry/wwwroot/CodeGen
+/Admin.NET/Admin.NET.Web.Entry/wwwroot/[Uu]pload
+/Admin.NET/Admin.NET.Web.Entry/wwwroot/[Aa]vatar
+/Admin.NET/Admin.NET.Web.Entry/wwwroot/[Ss]ignature
+/Admin.NET/Admin.NET.Web.Entry/wwwroot/[Cc]odeGen
 /Admin.NET/Admin.NET.Web.Entry/wwwroot/is-cache
 /Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj.user
 /Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj.user

+ 2 - 2
Admin.NET/Admin.NET.Application/Configuration/Database.json

@@ -1,4 +1,4 @@
-{
+{
   "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
 
   // 详细数据库配置见SqlSugar官网(第一个为默认库),极力推荐 PostgreSQL 数据库
@@ -13,7 +13,7 @@
         //"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL 库连接字符串
         //"ConnectionString": "Server=localhost;Database=xxx;Uid=xxx;Pwd=xxx;SslMode=None;", // MySql 库连接字符串",
         //"ConnectionString": "User Id=xxx; Password=xxx; Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))", // Oracle 库连接字符串
-        //"ConnectionString": "Server=localhost;Database=xxx;User Id=xxx;Password=xxx;", // SqlServer 库连接字符串
+        //"ConnectionString": "Server=localhost;Database=xxx;User Id=xxx;Password=xxx;Encrypt=True;TrustServerCertificate=True;", // SqlServer 库连接字符串
 
         //"SlaveConnectionConfigs": [ // 读写分离/主从
         //	{

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

@@ -17,17 +17,17 @@
     <PackageReference Include="AngleSharp" Version="1.1.2" />
     <PackageReference Include="AspectCore.Extensions.Reflection" Version="2.4.0" />
     <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
-    <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.15.4" />
-    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.5.5" />
-    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.5.5" />
-    <PackageReference Include="Furion.Pure" Version="4.9.5.5" />
+    <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.15.8" />
+    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.5.13" />
+    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.5.13" />
+    <PackageReference Include="Furion.Pure" Version="4.9.5.13" />
     <PackageReference Include="Hashids.net" Version="1.7.0" />
     <PackageReference Include="IPTools.China" Version="1.6.0" />
     <PackageReference Include="IPTools.International" Version="1.6.0" />
-    <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.7.1.1" />
+    <PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.2" />
+    <PackageReference Include="Magicodes.IE.Pdf" Version="2.7.5.2" />
+    <PackageReference Include="Magicodes.IE.Word" Version="2.7.5.2" />
+    <PackageReference Include="MailKit" Version="4.8.0" />
     <PackageReference Include="NewLife.Redis" Version="5.7.2024.801" />
     <PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
     <PackageReference Include="QRCoder" Version="1.6.0" />
@@ -35,11 +35,11 @@
     <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.2" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.5.0" />
-    <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.7.0" />
-    <PackageReference Include="SqlSugarCore" Version="5.1.4.167" />
+    <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.8.0" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.4.169" />
     <PackageReference Include="SSH.NET" Version="2024.1.0" />
     <PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.5" />
-    <PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1079" />
+    <PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1100" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
   </ItemGroup>
@@ -47,7 +47,7 @@
   <ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
     <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="6.0.15" />
     <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="6.0.15" />
-    <PackageReference Include="Lazy.Captcha.Core" Version="2.0.8" />
+    <PackageReference Include="Lazy.Captcha.Core" Version="2.0.6" />
     <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="6.0.33" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="6.0.33" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.33" />
@@ -55,9 +55,9 @@
   </ItemGroup>
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
-    <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="8.1.0" />
-    <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="8.1.0" />
-    <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" />
+    <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="8.2.0" />
+    <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="8.2.0" />
+    <PackageReference Include="Lazy.Captcha.Core" Version="2.0.8" />
     <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.8" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.8" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.8" />

+ 25 - 0
Admin.NET/Admin.NET.Core/Entity/SysUser.cs

@@ -312,4 +312,29 @@ public partial class SysUser : EntityTenant
     [SugarColumn(ColumnDescription = "电子签名", Length = 512)]
     [MaxLength(512)]
     public string? Signature { get; set; }
+
+    /// <summary>
+    /// 验证超级管理员类型,若账号类型为超级管理员则报错
+    /// </summary>
+    /// <param name="errorMsg">自定义错误消息</param>
+    public void ValidateIsSuperAdminAccountType(ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1014)
+    {
+        if (AccountType == AccountTypeEnum.SuperAdmin)
+        {
+            throw Oops.Oh(errorMsg);
+        }
+    }
+
+    /// <summary>
+    /// 验证用户Id是否相同,若用户Id相同则报错
+    /// </summary>
+    /// <param name="userId">用户Id</param>
+    /// <param name="errorMsg">自定义错误消息</param>
+    public void ValidateIsUserId(long userId, ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1001)
+    {
+        if (Id == userId)
+        {
+            throw Oops.Oh(errorMsg);
+        }
+    }
 }

+ 18 - 21
Admin.NET/Admin.NET.Core/Extension/EnumExtension.cs

@@ -31,7 +31,7 @@ public static class EnumExtension
             throw new ArgumentException("Type '" + enumType.Name + "' is not an enum.");
 
         // 查询缓存
-        var enumDic = EnumNameValueDict.ContainsKey(enumType) ? EnumNameValueDict[enumType] : new Dictionary<int, string>();
+        var enumDic = EnumNameValueDict.TryGetValue(enumType, out var value) ? value : new Dictionary<int, string>();
         if (enumDic.Count != 0)
             return enumDic;
         // 取枚举类型的Key/Value字典集合
@@ -57,7 +57,7 @@ public static class EnumExtension
         // 遍历字段数组获取key和name
         foreach (var enumField in enumFields)
         {
-            var intValue = (int)enumField.GetValue(enumType);
+            var intValue = (int)enumField.GetValue(enumType)!;
             enumDic[intValue] = enumField.Name;
         }
 
@@ -76,8 +76,8 @@ public static class EnumExtension
             throw new ArgumentException("Type '" + enumType.Name + "' is not an enum.");
 
         // 查询缓存
-        var enumDic = EnumDisplayValueDict.ContainsKey(enumType)
-            ? EnumDisplayValueDict[enumType]
+        var enumDic = EnumDisplayValueDict.TryGetValue(enumType, out var value)
+            ? value
             : new Dictionary<int, string>();
         if (enumDic.Count != 0)
             return enumDic;
@@ -105,7 +105,7 @@ public static class EnumExtension
         // 遍历字段数组获取key和name
         foreach (var enumField in enumFields)
         {
-            var intValue = (int)enumField.GetValue(enumType);
+            var intValue = (int)enumField.GetValue(enumType)!;
             var desc = enumField.GetDescriptionValue<DescriptionAttribute>();
             enumDic[intValue] = desc != null && !string.IsNullOrEmpty(desc.Description) ? desc.Description : enumField.Name;
         }
@@ -125,7 +125,7 @@ public static class EnumExtension
         _enumTypeDict ??= LoadEnumTypeDict(assembly);
 
         // 按名称查找
-        return _enumTypeDict.ContainsKey(typeName) ? _enumTypeDict[typeName] : null;
+        return _enumTypeDict.TryGetValue(typeName, out var value) ? value : null;
     }
 
     /// <summary>
@@ -149,10 +149,9 @@ public static class EnumExtension
     /// </summary>
     /// <param name="value"></param>
     /// <returns></returns>
-    public static string GetDescription(this System.Enum value)
+    public static string GetDescription(this Enum value)
     {
-        return value.GetType().GetMember(value.ToString()).FirstOrDefault()?.GetCustomAttribute<DescriptionAttribute>()
-            ?.Description;
+        return value.GetType().GetField(value.ToString())?.GetCustomAttribute<DescriptionAttribute>()?.Description;
     }
 
     /// <summary>
@@ -162,8 +161,7 @@ public static class EnumExtension
     /// <returns></returns>
     public static string GetDescription(this object value)
     {
-        return value.GetType().GetMember(value.ToString() ?? string.Empty).FirstOrDefault()
-            ?.GetCustomAttribute<DescriptionAttribute>()?.Description;
+        return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute<DescriptionAttribute>()?.Description;
     }
 
     /// <summary>
@@ -173,8 +171,7 @@ public static class EnumExtension
     /// <returns></returns>
     public static string GetTheme(this object value)
     {
-        return value.GetType().GetMember(value.ToString() ?? string.Empty).FirstOrDefault()
-            ?.GetCustomAttribute<ThemeAttribute>()?.Theme;
+        return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute<ThemeAttribute>()?.Theme;
     }
 
     /// <summary>
@@ -186,10 +183,10 @@ public static class EnumExtension
     {
         if (!type.IsEnum)
             throw new ArgumentException("Type '" + type.Name + "' is not an enum.");
-        var arr = System.Enum.GetNames(type);
+        var arr = Enum.GetNames(type);
         return arr.Select(sl =>
         {
-            var item = System.Enum.Parse(type, sl);
+            var item = Enum.Parse(type, sl);
             return new EnumEntity
             {
                 Name = item.ToString(),
@@ -210,8 +207,8 @@ public static class EnumExtension
     {
         if (!type.IsEnum)
             throw new ArgumentException("Type '" + type.Name + "' is not an enum.");
-        var arr = System.Enum.GetNames(type);
-        return arr.Select(name => (T)System.Enum.Parse(type, name)).ToList();
+        var arr = Enum.GetNames(type);
+        return arr.Select(name => (T)Enum.Parse(type, name)).ToList();
     }
 }
 
@@ -223,20 +220,20 @@ public class EnumEntity
     /// <summary>
     /// 枚举的描述
     /// </summary>
-    public string Describe { set; get; }
+    public string Describe { get; set; }
 
     /// <summary>
     /// 枚举的样式
     /// </summary>
-    public string Theme { set; get; }
+    public string Theme { get; set; }
 
     /// <summary>
     /// 枚举名称
     /// </summary>
-    public string Name { set; get; }
+    public string Name { get; set; }
 
     /// <summary>
     /// 枚举对象的值
     /// </summary>
-    public int Value { set; get; }
+    public int Value { get; set; }
 }

+ 2 - 2
Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs

@@ -333,8 +333,8 @@ public static partial class ObjectExtension
     {
         if (!email.TryValidate(ValidationTypes.EmailAddress).IsValid) return email;
 
-        var masks = mask.ToString().PadLeft(4, mask);
-        return email.Replace(@"^([^\.]+)\.?", $"$1{masks}$2");
+        var pos = email.IndexOf("@");
+        return Mask(email[..pos], mask) + email[pos..];
     }
 
     /// <summary>

+ 9 - 8
Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs

@@ -47,6 +47,7 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
         var account = httpContext.User.FindFirst(ClaimConst.Account)?.Value;
         var realName = httpContext.User.FindFirst(ClaimConst.RealName)?.Value;
         var tenantId = (httpContext.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong();
+        //var device = httpContext.GetClientDeviceInfo().Trim();
 
         if (userId < 0 || string.IsNullOrWhiteSpace(account)) return;
         var user = new SysOnlineUser
@@ -66,12 +67,11 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
         // 是否开启单用户登录
         if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysSingleLogin))
         {
-            _sysCacheService.Set(CacheConst.KeyUserOnline + user.UserId, user);
+            _sysCacheService.HashAdd(CacheConst.KeyUserOnline, "" + user.UserId, user);
         }
-        else
+        else  // 非单用户登录则绑定用户连接信息
         {
-            var device = httpContext.GetClientDeviceInfo().Trim();
-            _sysCacheService.Set(CacheConst.KeyUserOnline + user.UserId + device, user);
+            _sysCacheService.HashAdd(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId, user);
         }
 
         // 以租户Id进行分组
@@ -102,17 +102,18 @@ public class OnlineUserHub : Hub<IOnlineUserHub>
         var user = await _sysOnlineUerRep.AsQueryable().Filter("", true).FirstAsync(u => u.ConnectionId == Context.ConnectionId);
         if (user == null) return;
 
-        await _sysOnlineUerRep.DeleteAsync(u => u.Id == user.Id);
+        await _sysOnlineUerRep.DeleteByIdAsync(user.Id);
 
         // 是否开启单用户登录
         if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysSingleLogin))
         {
-            _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId);
+            _sysCacheService.HashDel<SysOnlineUser>(CacheConst.KeyUserOnline, "" + user.UserId);
+            // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId);
         }
         else
         {
-            var device = httpContext.GetClientDeviceInfo().Trim();
-            _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId + device);
+            _sysCacheService.HashDel<SysOnlineUser>(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId);
+            // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId + Context.ConnectionId);
         }
 
         // 通知当前组用户变动

+ 156 - 101
Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs

@@ -14,137 +14,192 @@ namespace Admin.NET.Core;
 public class EnumToDictJob : IJob
 {
     private readonly IServiceScopeFactory _scopeFactory;
-    private readonly IJsonSerializerProvider _jsonSerializer;
+    private const int OrderOffset = 10;
+    private const string DefaultTagType = "info";
 
-    public EnumToDictJob(IServiceScopeFactory scopeFactory, IJsonSerializerProvider jsonSerializer)
+    public EnumToDictJob(IServiceScopeFactory scopeFactory)
     {
         _scopeFactory = scopeFactory;
-        _jsonSerializer = jsonSerializer;
     }
 
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
         using var serviceScope = _scopeFactory.CreateScope();
         var sysEnumService = serviceScope.ServiceProvider.GetRequiredService<SysEnumService>();
+        // 获取数据库连接
         var db = serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
 
+        // 获取枚举类型列表
         var enumTypeList = sysEnumService.GetEnumTypeList();
         var enumCodeList = enumTypeList.Select(u => u.TypeName);
-        var sysDictTypeCodeList = await db.Queryable<SysDictType>().Where(u => enumCodeList.Contains(u.Code)).Select(u => u.Code).ToListAsync(stoppingToken);
-
+        // 查询数据库中已存在的枚举类型代码
+        var sysDictTypeList = await db.Queryable<SysDictType>()
+                                      .Includes(d => d.Children)
+                                      .Where(d => enumCodeList.Contains(d.Code))
+                                      .ToListAsync(stoppingToken);
         // 更新的枚举转换字典
-        var uEnumType = enumTypeList.Where(u => sysDictTypeCodeList.Contains(u.TypeName)).ToList();
-        var waitUpdateSysDictType = await db.Queryable<SysDictType>().Where(u => uEnumType.Any(a => a.TypeName == u.Code)).ToListAsync(stoppingToken);
-        var waitUpdateSysDictTypeDict = waitUpdateSysDictType.ToDictionary(u => u.Code, u => u);
-        var waitUpdateSysDictData = await db.Queryable<SysDictData>().Where(u => uEnumType.Any(a => a.TypeName == u.DictType.Code)).ToListAsync(stoppingToken);
-        var uSysDictType = new List<SysDictType>();
-        var uSysDictData = new List<SysDictData>();
-        if (uEnumType.Count > 0)
+        var updatedEnumCodes = sysDictTypeList.Select(u => u.Code);
+        var updatedEnumType = enumTypeList.Where(u => updatedEnumCodes.Contains(u.TypeName)).ToList();
+        var sysDictTypeDict = sysDictTypeList.ToDictionary(u => u.Code, u => u);
+        var (updatedDictTypes, updatedDictDatas, newSysDictDatas) = GetUpdatedDicts(updatedEnumType, sysDictTypeDict);
+
+        // 新增的枚举转换字典
+        var newEnumType = enumTypeList.Where(u => !updatedEnumCodes.Contains(u.TypeName)).ToList();
+        var (newDictTypes, newDictDatas) = GetNewSysDicts(newEnumType);
+
+        // 执行数据库操作
+        try
         {
-            uEnumType.ForEach(e =>
-            {
-                if (waitUpdateSysDictTypeDict.TryGetValue(e.TypeName, out SysDictType value))
-                {
-                    var uDictType = value;
-                    uDictType.Name = e.TypeDescribe;
-                    uDictType.Remark = e.TypeRemark;
-                    var uDictData = waitUpdateSysDictData.Where(u => u.DictTypeId == uDictType.Id).ToList();
-                    if (uDictData.Count > 0)
-                    {
-                        uDictData.ForEach(dictData =>
-                        {
-                            var enumData = e.EnumEntities.Where(u => dictData.Code == u.Name).FirstOrDefault();
-                            if (enumData != null)
-                            {
-                                dictData.Value = enumData.Value.ToString();
-                                dictData.Code = enumData.Name;
-                                dictData.OrderNo = enumData.Value + 10;
-                                dictData.Name = enumData.Describe;
-                                dictData.TagType = enumData.Theme != "" ? enumData.Theme : dictData.TagType != "" ? dictData.TagType : "info";
-                                uSysDictData.Add(dictData);
-                            }
-                        });
-                    }
-                    if (!uSysDictType.Any(u => u.Id == uDictType.Id))
-                        uSysDictType.Add(uDictType);
-                }
-            });
-            try
-            {
-                db.BeginTran();
+            await db.BeginTranAsync();
 
-                if (uSysDictType.Count > 0)
-                    await db.Updateable(uSysDictType).ExecuteCommandAsync(stoppingToken);
+            if (updatedDictTypes.Count > 0)
+                await db.Updateable(updatedDictTypes).ExecuteCommandAsync(stoppingToken);
 
-                if (uSysDictData.Count > 0)
-                    await db.Updateable(uSysDictData).ExecuteCommandAsync(stoppingToken);
+            if (updatedDictDatas.Count > 0)
+                await db.Updateable(updatedDictDatas).ExecuteCommandAsync(stoppingToken);
 
-                db.CommitTran();
-            }
-            catch (Exception error)
-            {
-                db.RollbackTran();
-                Log.Error($"{context.Trigger.Description}更新枚举转换字典入库错误:" + _jsonSerializer.Serialize(error));
-                throw new Exception($"{context.Trigger.Description}更新枚举转换字典入库错误");
-            }
+            if (newSysDictDatas.Count > 0)
+                await db.Insertable(newSysDictDatas).ExecuteCommandAsync(stoppingToken);
+
+            if (newDictTypes.Count > 0)
+                await db.Insertable(newDictTypes).ExecuteCommandAsync(stoppingToken);
+
+            if (newDictDatas.Count > 0)
+                await db.Insertable(newDictDatas).ExecuteCommandAsync(stoppingToken);
+
+            await db.CommitTranAsync();
         }
+        catch (Exception error)
+        {
+            await db.RollbackTranAsync();
+            Log.Error($"系统枚举转换字典操作错误:{error.Message}\n堆栈跟踪:{error.StackTrace}", error);
+            throw;
+        }
+        var originColor = Console.ForegroundColor;
+        Console.ForegroundColor = ConsoleColor.Green;
+        Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典");
+        Console.ForegroundColor = originColor;
+    }
 
-        // 新增的枚举转换字典
-        var iEnumType = enumTypeList.Where(u => !sysDictTypeCodeList.Contains(u.TypeName)).ToList();
-        if (iEnumType.Count > 0)
+    /// <summary>
+    /// 获取需要新增的字典列表
+    /// </summary>
+    /// <param name="addEnumType"></param>
+    /// <returns>
+    /// 一个元组,包含以下元素:
+    /// <list type="table">
+    ///     <item><term>SysDictTypes</term><description>字典类型列表</description></item>
+    ///     <item><term>SysDictDatas</term><description>字典数据列表</description></item>
+    /// </list>
+    /// </returns>
+    private (List<SysDictType>, List<SysDictData>) GetNewSysDicts(List<EnumTypeOutput> addEnumType)
+    {
+        var newDictType = new List<SysDictType>();
+        var newDictData = new List<SysDictData>();
+        if (addEnumType.Count <= 0)
+            return (newDictType, newDictData);
+
+        // 新增字典类型
+        newDictType = addEnumType.Select(u => new SysDictType
+        {
+            Id = YitIdHelper.NextId(),
+            Code = u.TypeName,
+            Name = u.TypeDescribe,
+            Remark = u.TypeRemark,
+            Status = StatusEnum.Enable
+        }).ToList();
+
+        // 新增字典数据
+        newDictData = addEnumType.Join(newDictType, t1 => t1.TypeName, t2 => t2.Code, (t1, t2) => new
         {
-            // 新增字典类型
-            var iDictType = iEnumType.Select(u => new SysDictType
+            Data = t1.EnumEntities.Select(u => new SysDictData
             {
                 Id = YitIdHelper.NextId(),
-                Code = u.TypeName,
-                Name = u.TypeDescribe,
-                Remark = u.TypeRemark,
-                Status = StatusEnum.Enable
-            }).ToList();
-            // 新增字典数据
-            var dictData = iEnumType.Join(iDictType, t1 => t1.TypeName, t2 => t2.Code, (t1, t2) => new
-            {
-                data = t1.EnumEntities.Select(u => new SysDictData
-                {
-                    Id = YitIdHelper.NextId(), // 性能优化,使用BulkCopyAsync必须手动获取Id
-                    DictTypeId = t2.Id,
-                    Name = u.Describe,
-                    Value = u.Value.ToString(),
-                    Code = u.Name,
-                    Remark = t2.Remark,
-                    OrderNo = u.Value + 10,
-                    TagType = u.Theme != "" ? u.Theme : "info",
-                }).ToList()
-            }).ToList();
-            var iDictData = new List<SysDictData>();
-            dictData.ForEach(item =>
-            {
-                iDictData.AddRange(item.data);
-            });
-            try
-            {
-                db.BeginTran();
+                DictTypeId = t2.Id,
+                Name = u.Describe,
+                Value = u.Value.ToString(),
+                Code = u.Name,
+                Remark = t2.Remark,
+                OrderNo = u.Value + OrderOffset,
+                TagType = u.Theme != "" ? u.Theme : DefaultTagType,
+            }).ToList()
+        }).SelectMany(x => x.Data).ToList();
+
+        return (newDictType, newDictData);
+    }
 
-                if (iDictType.Count > 0)
-                    await db.Insertable(iDictType).ExecuteCommandAsync(stoppingToken);
+    /// <summary>
+    /// 获取需要更新的字典列表
+    /// </summary>
+    /// <param name="updatedEnumType"></param>
+    /// <param name="sysDictTypeDict"></param>
+    /// <returns>
+    /// 一个元组,包含以下元素:
+    /// <list type="table">
+    ///     <item><term>SysDictTypes</term><description>更新字典类型列表</description>
+    ///     </item>
+    ///     <item><term>SysDictDatas</term><description>更新字典数据列表</description>
+    ///     </item>
+    ///     <item><term>SysDictDatas</term><description>新增字典数据列表</description>
+    ///     </item>
+    /// </list>
+    /// </returns>
+    private (List<SysDictType>, List<SysDictData>, List<SysDictData>) GetUpdatedDicts(List<EnumTypeOutput> updatedEnumType, Dictionary<string, SysDictType> sysDictTypeDict)
+    {
+        var updatedSysDictTypes = new List<SysDictType>();
+        var updatedSysDictData = new List<SysDictData>();
+        var newSysDictData = new List<SysDictData>();
+        foreach (var e in updatedEnumType)
+        {
+            if (!sysDictTypeDict.TryGetValue(e.TypeName, out var value))
+                continue;
 
-                if (iDictData.Count > 0)
-                    await db.Insertable(iDictData).ExecuteCommandAsync(stoppingToken);
+            var updatedDictType = value;
+            updatedDictType.Name = e.TypeDescribe;
+            updatedDictType.Remark = e.TypeRemark;
+            updatedSysDictTypes.Add(updatedDictType);
+            var updatedDictData = updatedDictType.Children.Where(u => u.DictTypeId == updatedDictType.Id).ToList();
 
-                db.CommitTran();
+            // 遍历需要更新的字典数据
+            foreach (var dictData in updatedDictData)
+            {
+                var enumData = e.EnumEntities.FirstOrDefault(u => dictData.Code == u.Name);
+                if (enumData != null)
+                {
+                    dictData.Value = enumData.Value.ToString();
+                    dictData.OrderNo = enumData.Value + OrderOffset;
+                    dictData.Name = enumData.Describe;
+                    dictData.TagType = enumData.Theme != "" ? enumData.Theme : dictData.TagType != "" ? dictData.TagType : DefaultTagType;
+                    updatedSysDictData.Add(dictData);
+                }
             }
-            catch (Exception error)
+
+            // 新增的枚举值名称列表
+            var newEnumDataNameList = e.EnumEntities.Select(u => u.Name).Except(updatedDictData.Select(u => u.Code));
+            foreach (var newEnumDataName in newEnumDataNameList)
             {
-                db.RollbackTran();
-                Log.Error($"{context.Trigger.Description}新增枚举转换字典入库错误:" + _jsonSerializer.Serialize(error));
-                throw new Exception($"{context.Trigger.Description}新增枚举转换字典入库错误");
+                var enumData = e.EnumEntities.FirstOrDefault(u => newEnumDataName == u.Name);
+                if (enumData != null)
+                {
+                    var dictData = new SysDictData
+                    {
+                        Id = YitIdHelper.NextId(),
+                        DictTypeId = updatedDictType.Id,
+                        Name = enumData.Describe,
+                        Value = enumData.Value.ToString(),
+                        Code = enumData.Name,
+                        Remark = updatedDictType.Remark,
+                        OrderNo = enumData.Value + OrderOffset,
+                        TagType = enumData.Theme != "" ? enumData.Theme : DefaultTagType,
+                    };
+                    dictData.TagType = enumData.Theme != "" ? enumData.Theme : dictData.TagType != "" ? dictData.TagType : DefaultTagType;
+                    newSysDictData.Add(dictData);
+                }
             }
+
+            // 删除的情况暂不处理
         }
 
-        var originColor = Console.ForegroundColor;
-        Console.ForegroundColor = ConsoleColor.Green;
-        Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典");
-        Console.ForegroundColor = originColor;
+        return (updatedSysDictTypes, updatedSysDictData, newSysDictData);
     }
 }

+ 4 - 6
Admin.NET/Admin.NET.Core/Job/LogJob.cs

@@ -26,15 +26,13 @@ public class LogJob : IJob
     {
         using var serviceScope = _scopeFactory.CreateScope();
 
-        var logVisRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogVis>>();
-        var logOpRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogOp>>();
-        var logDiffRep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysLogDiff>>();
+        var db = serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
         var sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
 
         var daysAgo = await sysConfigService.GetConfigValue<int>(ConfigConst.SysLogRetentionDays); // 日志保留天数
-        await logVisRep.CopyNew().AsDeleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除访问日志
-        await logOpRep.CopyNew().AsDeleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除操作日志
-        await logDiffRep.CopyNew().AsDeleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除差异日志
+        await db.Deleteable<SysLogVis>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除访问日志
+        await db.Deleteable<SysLogOp>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除操作日志
+        await db.Deleteable<SysLogDiff>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除差异日志
 
         string msg = $"【{DateTime.Now}】清理系统日志成功,删除 {daysAgo} 天前的日志数据!";
         var originColor = Console.ForegroundColor;

+ 5 - 5
Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs

@@ -28,11 +28,8 @@ public class OnlineUserJob : IJob
     {
         using var serviceScope = _scopeFactory.CreateScope();
 
-        var rep = serviceScope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysOnlineUser>>();
-        await rep.CopyNew().AsDeleteable().ExecuteCommandAsync(stoppingToken);
-
-        // 缓存租户列表
-        await serviceScope.ServiceProvider.GetRequiredService<SysTenantService>().CacheTenant();
+        var db = serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
+        await db.Deleteable<SysOnlineUser>().ExecuteCommandAsync(stoppingToken);
 
         string msg = $"【{DateTime.Now}】清理在线用户成功!服务已重启...";
         var originColor = Console.ForegroundColor;
@@ -42,5 +39,8 @@ public class OnlineUserJob : IJob
 
         // 自定义日志
         _logger.LogInformation(msg);
+
+        // 缓存租户列表
+        await serviceScope.ServiceProvider.GetRequiredService<SysTenantService>().CacheTenant();
     }
 }

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

@@ -345,7 +345,7 @@ public class SysAuthService : IDynamicApiController, ITransient
     /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("/api/swagger/checkUrl"), NonUnify]
-    [DisplayName("Swagger登录检查")]
+    [ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
     public int SwaggerCheckUrl()
     {
         return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
@@ -358,7 +358,7 @@ public class SysAuthService : IDynamicApiController, ITransient
     /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("/api/swagger/submitUrl"), NonUnify]
-    [DisplayName("Swagger登录提交")]
+    [ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
     public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
     {
         try

+ 138 - 37
Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs

@@ -4,7 +4,8 @@
 //
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
-using NewLife.Caching.Models;
+using NewLife.Reflection;
+using Newtonsoft.Json;
 
 namespace Admin.NET.Core.Service;
 
@@ -31,6 +32,7 @@ public class SysCacheService : IDynamicApiController, ISingleton
     /// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
     /// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
     /// <returns></returns>
+    [DisplayName("申请分布式锁")]
     public IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true)
     {
         try
@@ -82,6 +84,97 @@ public class SysCacheService : IDynamicApiController, ISingleton
         return _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value, expire);
     }
 
+    public async Task<TR> AdGetAsync<TR>(String cacheName, Func<Task<TR>> del, TimeSpan? expiry = default(TimeSpan?)) where TR : class
+    {
+        return await AdGetAsync<TR>(cacheName, del, new object[] { }, expiry);
+    }
+
+    public async Task<TR> AdGetAsync<TR, T1>(String cacheName, Func<T1, Task<TR>> del, T1 t1, TimeSpan? expiry = default(TimeSpan?)) where TR : class
+    {
+        return await AdGetAsync<TR>(cacheName, del, new object[] { t1 }, expiry);
+    }
+
+    public async Task<TR> AdGetAsync<TR, T1, T2>(String cacheName, Func<T1, T2, Task<TR>> del, T1 t1, T2 t2, TimeSpan? expiry = default(TimeSpan?)) where TR : class
+    {
+        return await AdGetAsync<TR>(cacheName, del, new object[] { t1, t2 }, expiry);
+    }
+
+    public async Task<TR> AdGetAsync<TR, T1, T2, T3>(String cacheName, Func<T1, T2, T3, Task<TR>> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default(TimeSpan?)) where TR : class
+    {
+        return await AdGetAsync<TR>(cacheName, del, new object[] { t1, t2, t3 }, expiry);
+    }
+
+    private async Task<T> AdGetAsync<T>(string cacheName, Delegate del, Object[] obs, TimeSpan? expiry) where T : class
+    {
+        var key = Key(cacheName, obs);
+        // 使用分布式锁
+        using (_cacheProvider.Cache.AcquireLock($@"lock:AdGetAsync:{cacheName}", 1000))
+        {
+            var value = Get<T>(key);
+            value ??= await ((dynamic)del).DynamicInvokeAsync(obs);
+            Set(key, value);
+            return value;
+        }
+    }
+
+    public T Get<T>(String cacheName, object t1)
+    {
+        return Get<T>(cacheName, new object[] { t1 });
+    }
+
+    public T Get<T>(String cacheName, object t1, object t2)
+    {
+        return Get<T>(cacheName, new object[] { t1, t2 });
+    }
+
+    public T Get<T>(String cacheName, object t1, object t2, object t3)
+    {
+        return Get<T>(cacheName, new object[] { t1, t2, t3 });
+    }
+
+    private T Get<T>(String cacheName, Object[] obs)
+    {
+        var key = cacheName + ":" + obs.Aggregate(string.Empty, (current, o) => current + $"<{o}>");
+        return Get<T>(key);
+    }
+
+    private static string Key(string cacheName, object[] obs)
+    {
+        foreach (var obj in obs)
+        {
+            if (obj is TimeSpan)
+            {
+                throw new Exception("缓存参数类型不能能是:TimeSpan类型");
+            }
+        }
+        StringBuilder sb = new StringBuilder(cacheName + ":");
+        foreach (var a in obs)
+        {
+            sb.Append($@"<{KeySingle(a)}>");
+        }
+        return sb.ToString();
+    }
+
+    public static string KeySingle(object t)
+    {
+        if (t.GetType().IsClass && !t.GetType().IsPrimitive)
+        {
+            return JsonConvert.SerializeObject(t);
+        }
+        return t?.ToString();
+    }
+
+    /// <summary>
+    /// 获取缓存的剩余生存时间
+    /// </summary>
+    /// <param name="key"></param>
+    /// <returns></returns>
+    [NonAction]
+    public TimeSpan GetExpire(string key)
+    {
+        return _cacheProvider.Cache.GetExpire(key);
+    }
+
     /// <summary>
     /// 获取缓存
     /// </summary>
@@ -197,9 +290,9 @@ public class SysCacheService : IDynamicApiController, ISingleton
     /// <param name="key"></param>
     /// <returns></returns>
     [NonAction]
-    public RedisHash<string, T> GetHashMap<T>(string key)
+    public IDictionary<String, T> GetHashMap<T>(string key)
     {
-        return _cacheProvider.Cache.GetDictionary<T>(key) as RedisHash<string, T>;
+        return _cacheProvider.Cache.GetDictionary<T>(key);
     }
 
     /// <summary>
@@ -213,7 +306,11 @@ public class SysCacheService : IDynamicApiController, ISingleton
     public bool HashSet<T>(string key, Dictionary<string, T> dic)
     {
         var hash = GetHashMap<T>(key);
-        return hash.HMSet(dic);
+        foreach (var v in dic)
+        {
+            hash.Add(v);
+        }
+        return true;
     }
 
     /// <summary>
@@ -241,8 +338,7 @@ public class SysCacheService : IDynamicApiController, ISingleton
     public List<T> HashGet<T>(string key, params string[] fields)
     {
         var hash = GetHashMap<T>(key);
-        var result = hash.HMGet(fields);
-        return result.ToList();
+        return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList();
     }
 
     /// <summary>
@@ -256,8 +352,12 @@ public class SysCacheService : IDynamicApiController, ISingleton
     public T HashGetOne<T>(string key, string field)
     {
         var hash = GetHashMap<T>(key);
-        var result = hash.HMGet(new string[] { field });
-        return result[0];
+        var value = hash.GetValue(field);
+        if (value == null)
+        {
+            return default(T);
+        }
+        return (T)hash.GetValue(field);
     }
 
     /// <summary>
@@ -270,7 +370,7 @@ public class SysCacheService : IDynamicApiController, ISingleton
     public IDictionary<string, T> HashGetAll<T>(string key)
     {
         var hash = GetHashMap<T>(key);
-        return hash.GetAll();
+        return hash;
     }
 
     /// <summary>
@@ -284,35 +384,36 @@ public class SysCacheService : IDynamicApiController, ISingleton
     public int HashDel<T>(string key, params string[] fields)
     {
         var hash = GetHashMap<T>(key);
-        return hash.HDel(fields);
+        fields.ToList().ForEach(t => hash.Remove(t));
+        return fields.Length;
     }
 
-    /// <summary>
-    /// 搜索HASH
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="key"></param>
-    /// <param name="searchModel"></param>
-    /// <returns></returns>
-    [NonAction]
-    public List<KeyValuePair<string, T>> HashSearch<T>(string key, SearchModel searchModel)
-    {
-        var hash = GetHashMap<T>(key);
-        return hash.Search(searchModel).ToList();
-    }
+    ///// <summary>
+    ///// 搜索HASH
+    ///// </summary>
+    ///// <typeparam name="T"></typeparam>
+    ///// <param name="key"></param>
+    ///// <param name="searchModel"></param>
+    ///// <returns></returns>
+    //[NonAction]
+    //public List<KeyValuePair<string, T>> HashSearch<T>(string key, SearchModel searchModel)
+    //{
+    //    var hash = GetHashMap<T>(key);
+    //    return hash.Search(searchModel).ToList();
+    //}
 
-    /// <summary>
-    /// 搜索HASH
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="key"></param>
-    /// <param name="pattern"></param>
-    /// <param name="count"></param>
-    /// <returns></returns>
-    [NonAction]
-    public List<KeyValuePair<string, T>> HashSearch<T>(string key, string pattern, int count)
-    {
-        var hash = GetHashMap<T>(key);
-        return hash.Search(pattern, count).ToList();
-    }
+    ///// <summary>
+    ///// 搜索HASH
+    ///// </summary>
+    ///// <typeparam name="T"></typeparam>
+    ///// <param name="key"></param>
+    ///// <param name="pattern"></param>
+    ///// <param name="count"></param>
+    ///// <returns></returns>
+    //[NonAction]
+    //public List<KeyValuePair<string, T>> HashSearch<T>(string key, string pattern, int count)
+    //{
+    //    var hash = GetHashMap<T>(key);
+    //    return hash.Search(pattern, count).ToList();
+    //}
 }

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs

@@ -387,7 +387,7 @@ public class SysCodeGenService : IDynamicApiController, ITransient
             if (File.Exists(downloadPath))
                 File.Delete(downloadPath);
             ZipFile.CreateFromDirectory(zipPath, downloadPath);
-            return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/CodeGen/{input.TableName}.zip" };
+            return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" };
         }
     }
 

+ 9 - 0
Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs

@@ -42,4 +42,13 @@ public class CreateSeedDataInput
     /// </summary>
     /// <example>Web.Application</example>
     public string Suffix { get; set; }
+
+    /// <summary>
+    /// 过滤已有数据
+    /// </summary>
+    /// <remarks>
+    /// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据
+    /// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。
+    /// </remarks>
+    public bool FilterExistingData { get; set; }
 }

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

@@ -351,7 +351,66 @@ public class SysDatabaseService : IDynamicApiController, ITransient
         orderField = dbColumnInfos.Where(u => u.DbColumnName.ToLower() == "id").FirstOrDefault();
         if (orderField != null)
             query.OrderBy(orderField.DbColumnName);
-        object records = query.ToList();
+        IEnumerable recordsTmp = (IEnumerable)query.ToList();
+        List<dynamic> records = recordsTmp.ToDynamicList();
+        // 过滤已存在的数据
+        if (input.FilterExistingData && records.Count() > 0)
+        {
+            // 获取实体类型-所有种数据数据类型
+            var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && u.FullName.EndsWith("." + input.EntityName))
+                .Where(u => !u.GetCustomAttributes<IgnoreTableAttribute>().Any())
+                .ToList();
+            if (entityTypes.Count == 1) // 只有一个实体匹配才能过滤
+            {
+                // 获取实体的主键对应的属性名称
+                var pkInfo = entityTypes[0].GetProperties().Where(u => u.GetCustomAttribute<SugarColumn>() != null && u.GetCustomAttribute<SugarColumn>().IsPrimaryKey).First();
+                if (pkInfo != null)
+                {
+                    var seedDataTypes = App.EffectiveTypes
+                        .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(
+                            i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)) && i.GenericTypeArguments[0] == entityTypes[0]
+                            )
+                        )
+                        .ToList();
+                    // 可能会重名的种子数据不作为过滤项
+                    string doNotFilterfullName1 = $"{input.Position}.SeedData.{input.SeedDataName}";
+                    string doNotFilterfullName2 = $"{input.Position}.{input.SeedDataName}"; // Core中的命名空间没有SeedData
+
+                    PropertyInfo idPropertySeedData = records[0].GetType().GetProperty("Id");
+
+                    for (int i = seedDataTypes.Count - 1; i >= 0; i--)
+                    {
+                        string fullName = seedDataTypes[i].FullName;
+                        if ((fullName == doNotFilterfullName1) || (fullName == doNotFilterfullName2))
+                            continue;
+                        // 删除重复数据
+                        var instance = Activator.CreateInstance(seedDataTypes[i]);
+                        var hasDataMethod = seedDataTypes[i].GetMethod("HasData");
+                        var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast<object>();
+                        if (seedData == null) continue;
+
+                        List<object> recordsToRemove = new List<object>();
+                        foreach (var record in records)
+                        {
+                            object recordId = pkInfo.GetValue(record);
+                            foreach (var d1 in seedData)
+                            {
+                                object dataId = idPropertySeedData.GetValue(d1);
+                                if (recordId != null && dataId != null && recordId.Equals(dataId))
+                                {
+                                    recordsToRemove.Add(record);
+                                    break;
+                                }
+                            }
+                        }
+                        foreach (var itemToRemove in recordsToRemove)
+                        {
+                            records.Remove(itemToRemove);
+                        }
+                    }
+                }
+            }
+        }
         var timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" };
         var recordsJSON = JsonConvert.SerializeObject(records, Formatting.Indented, timeConverter);
 

+ 3 - 4
Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs

@@ -42,7 +42,6 @@ public class SysDictDataService : IDynamicApiController, ITransient
     /// 获取字典值列表 🔖
     /// </summary>
     /// <returns></returns>
-    [UnitOfWork]
     [DisplayName("获取字典值列表")]
     public async Task<List<SysDictData>> GetList([FromQuery] GetDataDictDataInput input)
     {
@@ -99,7 +98,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("删除字典值")]
     public async Task DeleteDictData(DeleteDictDataInput input)
     {
-        var dictData = await _sysDictDataRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
+        var dictData = await _sysDictDataRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
 
         var dictTypeCode = await _sysDictDataRep.AsQueryable().Where(u => u.DictTypeId == dictData.Id).Select(u => u.DictType.Code).FirstAsync();
         _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");
@@ -115,7 +114,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("获取字典值详情")]
     public async Task<SysDictData> GetDetail([FromQuery] DictDataInput input)
     {
-        return await _sysDictDataRep.GetFirstAsync(u => u.Id == input.Id);
+        return await _sysDictDataRep.GetByIdAsync(input.Id);
     }
 
     /// <summary>
@@ -127,7 +126,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
     [DisplayName("修改字典值状态")]
     public async Task SetStatus(DictDataInput input)
     {
-        var dictData = await _sysDictDataRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
+        var dictData = await _sysDictDataRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
 
         var dictTypeCode = await _sysDictDataRep.AsQueryable().Where(u => u.DictTypeId == dictData.Id).Select(u => u.DictType.Code).FirstAsync();
         _sysCacheService.Remove($"{CacheConst.KeyDict}{dictTypeCode}");

+ 4 - 3
Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs

@@ -54,6 +54,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
+    [UnitOfWork]
     [DisplayName("获取字典类型-值列表")]
     public async Task<List<SysDictData>> GetDataList([FromQuery] GetDataDictTypeInput input)
     {
@@ -106,7 +107,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("删除字典类型")]
     public async Task DeleteDictType(DeleteDictTypeInput input)
     {
-        var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
 
         // 删除字典值
         await _sysDictTypeRep.DeleteAsync(dictType);
@@ -121,7 +122,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("获取字典类型详情")]
     public async Task<SysDictType> GetDetail([FromQuery] DictTypeInput input)
     {
-        return await _sysDictTypeRep.GetFirstAsync(u => u.Id == input.Id);
+        return await _sysDictTypeRep.GetByIdAsync(input.Id);
     }
 
     /// <summary>
@@ -133,7 +134,7 @@ public class SysDictTypeService : IDynamicApiController, ITransient
     [DisplayName("修改字典类型状态")]
     public async Task SetStatus(DictTypeInput input)
     {
-        var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
 
         _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
 

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

@@ -28,7 +28,8 @@ public class SysEnumService : IDynamicApiController, ITransient
     {
         var enumTypeList = App.EffectiveTypes.Where(t => t.IsEnum)
             .Where(t => _enumOptions.EntityAssemblyNames.Contains(t.Assembly.GetName().Name) || _enumOptions.EntityAssemblyNames.Any(name => t.Assembly.GetName().Name.Contains(name)))
-            .OrderBy(u => u.Name).OrderBy(u => u.FullName)
+            .Where(t => t.GetCustomAttributes(typeof(ErrorCodeTypeAttribute), false).Length == 0) // 排除错误代码类型
+            .OrderBy(u => u.Name).ThenBy(u => u.FullName)
             .ToList();
 
         var result = new List<EnumTypeOutput>();

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/Log/SysLogExService.cs

@@ -35,8 +35,8 @@ public class SysLogExService : IDynamicApiController, ITransient
             .WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName)
             .WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp)
             .WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed)
-            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == input.Status)
-            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != input.Status)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200")
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200")
             //.OrderBy(u => u.CreateTime, OrderByType.Desc)
             .IgnoreColumns(u => new { u.RequestParam, u.ReturnResult, u.Message })
             .OrderBuilder(input)

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/Log/SysLogVisService.cs

@@ -34,8 +34,8 @@ public class SysLogVisService : IDynamicApiController, ITransient
             .WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName)
             .WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp)
             .WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed)
-            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == input.Status)
-            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != input.Status)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200")
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200")
             .OrderBy(u => u.CreateTime, OrderByType.Desc)
             .ToPagedListAsync(input.Page, input.PageSize);
     }

+ 10 - 13
Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs

@@ -165,11 +165,9 @@ public class SysUserService : IDynamicApiController, ITransient
     [DisplayName("删除用户")]
     public virtual async Task DeleteUser(DeleteUserInput input)
     {
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
-        if (user.AccountType == AccountTypeEnum.SuperAdmin)
-            throw Oops.Oh(ErrorCodeEnum.D1014);
-        if (user.Id == _userManager.UserId)
-            throw Oops.Oh(ErrorCodeEnum.D1001);
+        var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+        user.ValidateIsSuperAdminAccountType();
+        user.ValidateIsUserId(_userManager.UserId);
 
         // 若账号为租户默认账号则禁止删除
         var isTenantUser = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().IsAnyAsync(u => u.UserId == input.Id);
@@ -203,7 +201,7 @@ public class SysUserService : IDynamicApiController, ITransient
     [DisplayName("查看用户基本信息")]
     public virtual async Task<SysUser> GetBaseInfo()
     {
-        return await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
+        return await _sysUserRep.GetByIdAsync(_userManager.UserId);
     }
 
     /// <summary>
@@ -223,16 +221,15 @@ public class SysUserService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
+    [UnitOfWork]
     [DisplayName("设置用户状态")]
     public virtual async Task<int> SetStatus(UserInput input)
     {
         if (_userManager.UserId == input.Id)
             throw Oops.Oh(ErrorCodeEnum.D1026);
 
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
-        if (user.AccountType == AccountTypeEnum.SuperAdmin)
-            throw Oops.Oh(ErrorCodeEnum.D1015);
-
+        var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+        user.ValidateIsSuperAdminAccountType(ErrorCodeEnum.D1015);
         if (!Enum.IsDefined(typeof(StatusEnum), input.Status))
             throw Oops.Oh(ErrorCodeEnum.D3005);
 
@@ -282,7 +279,7 @@ public class SysUserService : IDynamicApiController, ITransient
         input.PasswordOld = CryptogramUtil.SM2Decrypt(input.PasswordOld);
         input.PasswordNew = CryptogramUtil.SM2Decrypt(input.PasswordNew);
 
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+        var user = await _sysUserRep.GetByIdAsync(_userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
         if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
         {
             if (user.Password != MD5Encryption.Encrypt(input.PasswordOld))
@@ -320,7 +317,7 @@ public class SysUserService : IDynamicApiController, ITransient
     [DisplayName("重置用户密码")]
     public virtual async Task<string> ResetPwd(ResetPwdUserInput input)
     {
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+        var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
         var password = await _sysConfigService.GetConfigValue<string>(ConfigConst.SysPassword);
         user.Password = CryptogramUtil.Encrypt(password);
         await _sysUserRep.AsUpdateable(user).UpdateColumns(u => u.Password).ExecuteCommandAsync();
@@ -340,7 +337,7 @@ public class SysUserService : IDynamicApiController, ITransient
     [DisplayName("解除登录锁定")]
     public virtual async Task UnlockLogin(UnlockLoginInput input)
     {
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+        var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009);
 
         // 清空密码错误次数
         var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";

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

@@ -106,4 +106,44 @@ public class DeleteMessageTemplateInput
     /// </summary>
     [Required(ErrorMessage = "订阅模板Id不能为空")]
     public string TemplateId { get; set; }
+}
+
+public class UploadAvatarInput
+{
+    /// <summary>
+    /// 小程序用户身份标识
+    /// </summary>
+    [Required(ErrorMessage = "OpenId不能为空")]
+    public string OpenId { get; set; }
+
+    /// <summary>
+    /// 文件
+    /// </summary>
+    [Required]
+    public IFormFile File { get; set; }
+
+    /// <summary>
+    /// 文件类型
+    /// </summary>
+    public string FileType { get; set; }
+
+    /// <summary>
+    /// 文件路径
+    /// </summary>
+    public string Path { get; set; }
+}
+
+public class SetNickNameInput
+{
+    /// <summary>
+    /// 小程序用户身份标识
+    /// </summary>
+    [Required(ErrorMessage = "OpenId不能为空")]
+    public string OpenId { get; set; }
+
+    /// <summary>
+    /// 昵称
+    /// </summary>
+    [Required(ErrorMessage = "昵称不能为空")]
+    public string NickName { get; set; }
 }

+ 4 - 0
Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs

@@ -70,6 +70,10 @@ public class SysWechatService : IDynamicApiController, ITransient
             wxUser = resUserInfo.Adapt<SysWechatUser>();
             wxUser.Avatar = resUserInfo.HeadImageUrl;
             wxUser.NickName = resUserInfo.Nickname;
+            wxUser.OpenId = resOAuth2.OpenId;
+            wxUser.UnionId = resOAuth2.UnionId;
+            wxUser.AccessToken = resOAuth2.AccessToken;
+            wxUser.RefreshToken = resOAuth2.RefreshToken;
             wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync();
         }
         else

+ 56 - 2
Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs

@@ -15,14 +15,17 @@ public class SysWxOpenService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
     private readonly SysConfigService _sysConfigService;
     private readonly WechatApiClient _wechatApiClient;
+    private readonly SysFileService _sysFileService;
 
     public SysWxOpenService(SqlSugarRepository<SysWechatUser> sysWechatUserRep,
         SysConfigService sysConfigService,
-        WechatApiClientFactory wechatApiClientFactory)
+        WechatApiClientFactory wechatApiClientFactory,
+        SysFileService sysFileService)
     {
         _sysWechatUserRep = sysWechatUserRep;
         _sysConfigService = sysConfigService;
         _wechatApiClient = wechatApiClientFactory.CreateWxOpenClient();
+        _sysFileService = sysFileService;
     }
 
     /// <summary>
@@ -114,7 +117,7 @@ public class SysWxOpenService : IDynamicApiController, ITransient
     [DisplayName("微信小程序登录OpenId")]
     public async Task<dynamic> WxOpenIdLogin(WxOpenIdLoginInput input)
     {
-        var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId);
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
         if (wxUser == null)
             throw Oops.Oh("微信小程序登录失败");
 
@@ -131,6 +134,57 @@ public class SysWxOpenService : IDynamicApiController, ITransient
         };
     }
 
+    /// <summary>
+    /// 上传小程序头像
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("上传小程序头像")]
+    public async Task<SysFile> UploadAvatar([FromForm] UploadAvatarInput input)
+    {
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
+        if (wxUser == null)
+            throw Oops.Oh("未找到用户上传失败");
+
+        var res = await _sysFileService.UploadFile(new FileUploadInput { File = input.File, FileType = input.FileType, Path = input.Path });
+        wxUser.Avatar = res.Url;
+        await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
+
+        return res;
+    }
+
+    /// <summary>
+    /// 设置小程序用户昵称
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost]
+    public async Task SetNickName(SetNickNameInput input)
+    {
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId);
+        if (wxUser == null)
+            throw Oops.Oh("未找到用户信息设置失败");
+        wxUser.NickName = input.NickName;
+        await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
+        return;
+    }
+
+    /// <summary>
+    /// 获取小程序用户信息
+    /// </summary>
+    /// <param name="openid"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    public async Task<dynamic> GetUserInfo(string openid)
+    {
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == openid);
+        if (wxUser == null)
+            throw Oops.Oh("未找到用户信息获取失败");
+        return new { nickName = wxUser.NickName, avator = wxUser.Avatar };
+    }
+
     /// <summary>
     /// 获取订阅消息模板列表 🔖
     /// </summary>

+ 29 - 1
Admin.NET/Admin.NET.Web.Core/Startup.cs

@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.ResponseCompression;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Newtonsoft.Json;
@@ -25,7 +26,7 @@ using OnceMi.AspNetCore.OSS;
 using SixLabors.ImageSharp.Web.DependencyInjection;
 using System;
 using System.Threading.Tasks;
-
+using System.Linq;
 namespace Admin.NET.Web.Core;
 
 public class Startup : AppStartup
@@ -201,10 +202,37 @@ public class Startup : AppStartup
         // 设置默认查询器China和International
         //IpToolSettings.DefalutSearcherType = IpSearcherType.China;
         IpToolSettings.DefalutSearcherType = IpSearcherType.International;
+        // 第一步: 配置gzip与br的压缩等级为最优
+        //services.Configure<BrotliCompressionProviderOptions>(options =>
+        //{
+        //    options.Level = CompressionLevel.Optimal;
+        //});
+        //services.Configure<GzipCompressionProviderOptions>(options =>
+        //{
+        //    options.Level = CompressionLevel.Optimal;
+        //});
+        //注册压缩响应
+        services.AddResponseCompression((options) =>
+        {
+            options.EnableForHttps = true;
+            options.Providers.Add<BrotliCompressionProvider>();
+            options.Providers.Add<GzipCompressionProvider>();
+            // 指定压缩的大小阈值,这里设置为1024字节
+            // 扩展一些类型 (MimeTypes中有一些基本的类型,可以打断点看看)
+            options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
+            {
+                    "text/html; charset=utf-8",
+                    "application/xhtml+xml",
+                    "application/atom+xml",
+                    "image/svg+xml"
+             });
+        });
     }
 
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
     {
+        // 响应压缩
+        app.UseResponseCompression();
         app.UseForwardedHeaders();
 
         if (env.IsDevelopment())

BIN
Admin.NET/Admin.NET.Web.Entry/wwwroot/images/logo.png


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

@@ -109,7 +109,7 @@ if (@column.QueryWhether == "Y"){
 } else {
             @:.Select<@(@Model.ClassName)Output>();
 }
-		return await query.OrderBuilder(input, "u.").ToPagedListAsync(input.Page, input.PageSize);
+		return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
     }
 
     /// <summary>

BIN
Admin.NET/Admin.NET.Web.Entry/wwwroot/upload/logo.png


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

@@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.ReZero", "
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.ApprovalFlow", "Plugins\Admin.NET.Plugin.ApprovalFlow\Admin.NET.Plugin.ApprovalFlow.csproj", "{902A91A7-5EF0-4A63-BC2C-9B783DC00880}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.K3Cloud", "Plugins\Admin.NET.Plugin.K3Cloud\Admin.NET.Plugin.K3Cloud.csproj", "{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -64,6 +66,10 @@ Global
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.Build.0 = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -73,6 +79,7 @@ Global
 		{F6A002AD-CF7F-4771-8597-F12A50A93DAA} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 		{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 		{902A91A7-5EF0-4A63-BC2C-9B783DC00880} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+		{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3}

+ 23 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Admin.NET.Plugin.K3Cloud.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
+    <NoWarn>1701;1702;1591;8632</NoWarn>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>disable</Nullable>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+    <Copyright>Admin.NET</Copyright>
+    <Description>Admin.NET 通用权限开发平台</Description>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Update="Configuration\**">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
+  </ItemGroup>
+
+</Project>

+ 20 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Configuration/K3Cloud.json

@@ -0,0 +1,20 @@
+{
+  "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
+
+  "K3Cloud": {
+    // ERP地址
+    "Url": "http://127.0.0.1/k3cloud/",
+    // 帐套Id(数据中心ID)
+    "AcctID": "XXXXXXXXX",
+    // 应用Id
+    "AppId": "XXXXXXXX",
+    // 应用密钥
+    "AppKey": "XXX",
+    // 用户名称
+    "UserName": "XXX",
+    // 用户密码
+    "UserPassword": "XXXX@2024",
+    // 语言代码
+    "LanguageCode": "2052"
+  }
+}

+ 8 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/GlobalUsings.cs

@@ -0,0 +1,8 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+global using Furion;
+global using Furion.ConfigurableOptions;

+ 45 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Option/K3CloudOptions.cs

@@ -0,0 +1,45 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Plugin.K3Cloud;
+
+public sealed class K3CloudOptions : IConfigurableOptions
+{
+    /// <summary>
+    /// ERP业务站点地址
+    /// </summary>
+    public string Url { get; set; }
+
+    /// <summary>
+    /// 帐套Id(数据中心ID)
+    /// </summary>
+    public string AcctID { get; set; }
+
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    public string AppId { get; set; }
+
+    /// <summary>
+    /// 应用密钥
+    /// </summary>
+    public string AppKey { get; set; }
+
+    /// <summary>
+    /// 用户名称
+    /// </summary>
+    public string UserName { get; set; }
+
+    /// <summary>
+    /// 用户密码
+    /// </summary>
+    public string UserPassword { get; set; }
+
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LanguageCode { get; set; }
+}

+ 23 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudBaeInput.cs

@@ -0,0 +1,23 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Plugin.K3Cloud.Service;
+
+/// <summary>
+/// ERP基础入参
+/// </summary>
+public class K3CloudBaeInput<T>
+{
+    /// <summary>
+    /// 表单Id
+    /// </summary>
+    public string formid { get; set; }
+
+    /// <summary>
+    /// 数据包
+    /// </summary>
+    public T data { get; set; }
+}

+ 12 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginInput.cs

@@ -0,0 +1,12 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Plugin.K3Cloud.Service;
+
+public class K3CloudLoginInput
+{
+    public List<string> parameters { get; set; }
+}

+ 62 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginOutput.cs

@@ -0,0 +1,62 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Plugin.K3Cloud.Service;
+
+public class K3CloudLoginOutput
+{
+    public string Message { get; set; }
+    public string MessageCode { get; set; }
+    public ErpLoginResultType LoginResultType { get; set; }
+}
+
+public enum ErpLoginResultType
+{
+    /// <summary>
+    /// 激活
+    /// </summary>
+    Activation = -7,
+
+    /// <summary>
+    /// 云通行证未绑定Cloud账号
+    /// </summary>
+    EntryCloudUnBind = -6,
+
+    /// <summary>
+    /// 需要表单处理
+    /// </summary>
+    DealWithForm = -5,
+
+    /// <summary>
+    /// 登录警告
+    /// </summary>
+    Wanning = -4,
+
+    /// <summary>
+    /// 密码验证不通过(强制的)
+    /// </summary>
+    PWInvalid_Required = -3,
+
+    /// <summary>
+    /// 密码验证不通过(可选的)
+    /// </summary>
+    PWInvalid_Optional = -2,
+
+    /// <summary>
+    /// 登录失败
+    /// </summary>
+    Failure = -1,
+
+    /// <summary>
+    /// 用户或密码错误
+    /// </summary>
+    PWError = 0,
+
+    /// <summary>
+    /// 登录成功
+    /// </summary>
+    Success = 1
+}

+ 66 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudPushResultOutput.cs

@@ -0,0 +1,66 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Plugin.K3Cloud.Service;
+
+public class K3CloudPushResultOutput
+{
+    public ErpPushResultInfo Result { get; set; }
+}
+
+public class ErpPushResultInfo
+{
+    /// <summary>
+    /// Id
+    /// </summary>
+    public object? Id { get; set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    public string? Number { get; set; }
+
+    public ErpPushResultInfo_ResponseStatus ResponseStatus { get; set; }
+}
+
+public class ErpPushResultInfo_ResponseStatus
+{
+    public bool IsSuccess { get; set; }
+    public int? ErrorCode { get; set; }
+
+    /// <summary>
+    /// 错误代码MsgCode说明
+    ///0:默认
+    ///1:上下文丢失 会话过期
+    ///2:没有权限
+    ///3:操作标识为空
+    ///4:异常
+    ///5:单据标识为空
+    ///6:数据库操作失败
+    ///7:许可错误
+    ///8:参数错误
+    ///9:指定字段/值不存在
+    ///10:未找到对应数据
+    ///11:验证失败
+    ///12:不可操作
+    ///13:网控冲突
+    ///14:调用限制
+    ///15:禁止管理员登录
+    /// </summary>
+    public int? MsgCode { get; set; }
+
+    /// <summary>
+    /// 如果失败,具体失败原因
+    /// </summary>
+    public List<ErpPushResultInfo_Errors> Errors { get; set; }
+}
+
+public class ErpPushResultInfo_Errors
+{
+    public string FieldName { get; set; }
+    public string Message { get; set; }
+    public int DIndex { get; set; }
+}

+ 52 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/IK3CloudApi.cs

@@ -0,0 +1,52 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Furion.RemoteRequest;
+
+namespace Admin.NET.Plugin.K3Cloud.Service;
+
+/// <summary>
+/// 金蝶云星空ERP接口
+/// </summary>
+[Client("K3Cloud")]
+public interface IK3CloudApi : IHttpDispatchProxy
+{
+    /// <summary>
+    /// 验证用户
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="action"></param>
+    /// <returns></returns>
+    [Post("Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc")]
+    Task<K3CloudLoginOutput> ValidateUser([Body] K3CloudLoginInput input, [Interceptor(InterceptorTypes.Response)] Action<HttpClient, HttpResponseMessage> action = default);
+
+    /// <summary>
+    /// 保存表单
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="action"></param>
+    /// <returns></returns>
+    [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc")]
+    Task<K3CloudPushResultOutput> Save<T>([Body] K3CloudBaeInput<T> input, [Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);
+
+    /// <summary>
+    /// 提交表单
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="action"></param>
+    /// <returns></returns>
+    [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Submit.common.kdsvc")]
+    Task<K3CloudPushResultOutput> Submit<T>([Body] K3CloudBaeInput<T> input, [Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);
+
+    /// <summary>
+    /// 审核表单
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="action"></param>
+    /// <returns></returns>
+    [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Audit.common.kdsvc")]
+    Task<K3CloudPushResultOutput> Audit<T>([Body] K3CloudBaeInput<T> input, [Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);
+}

+ 31 - 0
Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs

@@ -0,0 +1,31 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Admin.NET.Plugin.K3Cloud;
+
+[AppStartup(100)]
+public class Startup : AppStartup
+{
+    public void ConfigureServices(IServiceCollection services)
+    {
+        services.AddConfigurableOptions<K3CloudOptions>();
+        services.AddRemoteRequest(options =>
+        {
+            options.AddHttpClient("K3Cloud", u =>
+            {
+                u.BaseAddress = new Uri(App.GetConfig<K3CloudOptions>("K3Cloud", true).Url);
+            });
+        });
+    }
+
+    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+    {
+    }
+}

+ 2 - 1
Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj

@@ -24,7 +24,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Rezero.Api" Version="1.7.9" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.11.0" />
+    <PackageReference Include="Rezero.Api" Version="1.7.12" />
   </ItemGroup>
 
   <ItemGroup>

+ 6 - 2
README.md

@@ -1,4 +1,8 @@
-<div align="center"><h1>Admin.NET</h1></div>
+<div align="center">
+    <p align="center">
+        <img src="./Web/src/assets/logo.png" height="128" alt="logo"/>
+    </p>
+</div>
 <div align="center"><h3>站在巨人肩膀上的 .NET 通用权限开发框架</h3></div>
 
 <div align="center">
@@ -10,7 +14,7 @@
 </div>
 
 ## 🎁框架介绍
-基于 .NET6 (Furion/SqlSugar) 实现的通用权限开发框架,前端采用 Vue3+Element-plus+Vite5,整合众多优秀技术和框架,模块插件式开发。集成多租户、缓存、数据校验、鉴权、事件总线、动态API、通讯、远程请求、任务调度、打印等众多黑科技。代码结构简单清晰,注释详尽,易于上手与二次开发,即便是复杂业务逻辑也能迅速实现,真正实现“开箱即用”。
+Admin.NET 是基于 .NET6 (Furion/SqlSugar) 实现的通用权限开发框架,前端采用 Vue3+Element-plus+Vite5,整合众多优秀技术和框架,模块插件式开发。集成多租户、缓存、数据校验、鉴权、事件总线、动态API、通讯、远程请求、任务调度、打印等众多黑科技。代码结构简单清晰,注释详尽,易于上手与二次开发,即便是复杂业务逻辑也能迅速实现,真正实现“开箱即用”。
 
 面向中小企业快速开发平台框架,框架采用主流技术开发设计,前后端分离架构模式。完美适配国产化软硬件环境,支持国产中间件、国产数据库、麒麟操作系统、Windows、Linux部署使用;集成国密加解密插件,使用SM2、SM3、SM4等国密算法进行签名、数据完整性保护;软件层面全面遵循等级保护测评要求,完全符合等保、密评要求。
 

+ 67 - 0
Web/api_build/build_api.sh

@@ -0,0 +1,67 @@
+#!/bin/sh
+# 红色信息
+function echoRedInfo() {
+    echo -e "\e[31m$@\e[0m"
+}
+# 绿色信息
+function echoGreenInfo() {
+    echo -e "\e[32m$@\e[0m"
+}
+# 蓝色信息
+function echoBlueInfo() {
+    echo -e "\e[34m$@\e[0m"
+}
+url1="http://172.18.32.33:5050"
+url2="http://127.0.0.1:5050"
+url3="http://localhost:5005"
+# 打印菜单
+echoBlueInfo "请选择Swagger地址:"
+echoBlueInfo "(1) $url1"
+echoBlueInfo "(2) $url2"
+echoBlueInfo "(3) $url3"
+read -p "请输入选项 [1-3]: " choice
+
+currPath=$(pwd)
+parentPath=$(dirname "$currPath")
+apiServicesPath=${parentPath}/src/api-services/
+
+echo "生成目录 ${apiServicesPath}"
+
+# 判断目录是否存在
+if test -d "$apiServicesPath"; then
+  echo "删除目录 api-services"
+  rm -rf "${apiServicesPath}"
+fi
+
+echo "开始生成 api-services"
+
+
+# 检查用户输入并执行相应操作
+case $choice in
+    1)
+        echoGreenInfo "您选择了: $url1"
+        java -jar "${currPath}"/swagger-codegen-cli.jar generate -i $url1/swagger/All%20Groups/swagger.json -l typescript-axios -o "${apiServicesPath}"
+        ;;
+    2)
+        echoGreenInfo "您选择了: $url2"
+        java -jar "${currPath}"/swagger-codegen-cli.jar generate -i $url2/swagger/All%20Groups/swagger.json -l typescript-axios -o "${apiServicesPath}"
+        ;;
+    3)
+        echoGreenInfo "您选择了: $url3"
+        java -jar "${currPath}"/swagger-codegen-cli.jar generate -i $url3/swagger/All%20Groups/swagger.json -l typescript-axios -o "${apiServicesPath}"
+        ;;
+    *)
+        echoRedInfo "无效的选项,请输入[1-3]。"
+        exit 1
+        ;;
+esac
+rm -rf "${apiServicesPath}".swagger-codegen
+rm -f "${apiServicesPath}".gitignore
+rm -f "${apiServicesPath}".npmignore
+rm -f "${apiServicesPath}".swagger-codegen-ignore
+rm -f "${apiServicesPath}"git_push.sh
+rm -f "${apiServicesPath}"package.json
+rm -f "${apiServicesPath}"README.md
+rm -f "${apiServicesPath}"tsconfig.json
+
+echoGreenInfo "生成结束"

+ 26 - 27
Web/package.json

@@ -2,7 +2,7 @@
 	"name": "admin.net",
 	"type": "module",
 	"version": "2.4.33",
-	"lastBuildTime": "2024.08.30",
+	"lastBuildTime": "2024.10.03",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
 	"author": "zuohuaijun",
 	"license": "MIT",
@@ -21,50 +21,49 @@
 		"@vue-office/docx": "^1.6.2",
 		"@vue-office/excel": "^1.7.11",
 		"@vue-office/pdf": "^2.0.2",
-		"@vueuse/core": "^11.0.3",
+		"@vueuse/core": "^11.1.0",
 		"@wangeditor/editor": "^5.1.23",
 		"@wangeditor/editor-for-vue": "^5.1.12",
 		"animate.css": "^4.1.1",
 		"async-validator": "^4.2.5",
-		"axios": "^1.7.5",
+		"axios": "^1.7.7",
 		"countup.js": "^2.8.0",
 		"cropperjs": "^1.6.2",
 		"echarts": "^5.5.1",
 		"echarts-gl": "^2.0.9",
 		"echarts-wordcloud": "^2.1.0",
-		"element-plus": "^2.8.1",
-		"ezuikit": "^1.0.0",
-		"ezuikit-js": "^8.0.11",
+		"element-plus": "^2.8.4",
+		"ezuikit-js": "^8.0.12-alpha.3",
 		"js-cookie": "^3.0.5",
 		"js-table2excel": "^1.1.2",
 		"jsplumb": "^2.15.6",
 		"lodash-es": "^4.17.21",
-		"md-editor-v3": "^4.19.2",
+		"md-editor-v3": "^4.20.2",
 		"mitt": "^3.0.1",
-		"monaco-editor": "^0.51.0",
+		"monaco-editor": "^0.52.0",
 		"mqtt": "^4.3.8",
 		"nprogress": "^0.2.0",
-		"pinia": "^2.2.2",
+		"pinia": "^2.2.4",
 		"print-js": "^1.6.0",
 		"push.js": "^1.0.12",
 		"qrcodejs2-fixes": "^0.0.2",
 		"qs": "^6.13.0",
-		"relation-graph": "^2.2.3",
+		"relation-graph": "^2.2.6",
 		"screenfull": "^6.0.2",
 		"sm-crypto-v2": "^1.9.2",
-		"sortablejs": "^1.15.2",
+		"sortablejs": "^1.15.3",
 		"splitpanes": "^3.1.5",
 		"vcrontab-3": "^3.3.22",
 		"vform3-builds": "^3.0.10",
-		"vue": "^3.4.38",
+		"vue": "^3.5.10",
 		"vue-clipboard3": "^2.0.0",
 		"vue-demi": "^0.14.6",
 		"vue-draggable-plus": "^0.5.3",
 		"vue-grid-layout": "3.0.0-beta1",
-		"vue-i18n": "^9.14.0",
+		"vue-i18n": "^10.0.3",
 		"vue-json-pretty": "^2.4.0",
 		"vue-plugin-hiprint": "0.0.57-beta27",
-		"vue-router": "^4.4.3",
+		"vue-router": "^4.4.5",
 		"vue-signature-pad": "^3.0.2",
 		"vue3-tree-org": "^4.2.2",
 		"xlsx-js-style": "^1.2.0"
@@ -75,24 +74,24 @@
 		"@types/node": "^20.14.15",
 		"@types/nprogress": "^0.2.3",
 		"@types/sortablejs": "^1.15.8",
-		"@typescript-eslint/eslint-plugin": "^8.3.0",
-		"@typescript-eslint/parser": "^8.3.0",
-		"@vitejs/plugin-vue": "^5.1.3",
+		"@typescript-eslint/eslint-plugin": "^8.8.0",
+		"@typescript-eslint/parser": "^8.8.0",
+		"@vitejs/plugin-vue": "^5.1.4",
 		"@vitejs/plugin-vue-jsx": "^4.0.1",
-		"@vue/compiler-sfc": "^3.4.38",
-		"code-inspector-plugin": "^0.16.0",
-		"eslint": "^9.9.1",
-		"eslint-plugin-vue": "^9.27.0",
-		"globals": "^15.9.0",
+		"@vue/compiler-sfc": "^3.5.10",
+		"code-inspector-plugin": "^0.16.1",
+		"eslint": "^9.11.1",
+		"eslint-plugin-vue": "^9.28.0",
+		"globals": "^15.10.0",
 		"less": "^4.2.0",
 		"prettier": "^3.3.3",
 		"rollup-plugin-visualizer": "^5.12.0",
-		"sass": "^1.77.8",
-		"terser": "^5.31.6",
-		"typescript": "^5.5.4",
-		"vite": "^5.4.2",
+		"sass": "^1.79.4",
+		"terser": "^5.34.1",
+		"typescript": "^5.6.2",
+		"vite": "^5.4.8",
 		"vite-plugin-cdn-import": "^1.0.1",
-		"vite-plugin-compression2": "^1.2.0",
+		"vite-plugin-compression2": "^1.3.0",
 		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vue-eslint-parser": "^9.4.3"
 	},

BIN
Web/public/favicon.ico


+ 8 - 8
Web/src/api-services/apis/sys-database-api.ts

@@ -75,7 +75,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -123,7 +123,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -269,7 +269,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -317,7 +317,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -414,7 +414,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -462,7 +462,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -602,7 +602,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {
@@ -650,7 +650,7 @@ export const SysDatabaseApiAxiosParamCreator = function (configuration?: Configu
             localVarUrlObj.search = (new URLSearchParams(query)).toString();
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            const needsSerialization = (typeof body !== "string") || (localVarRequestOptions.headers && localVarRequestOptions.headers['Content-Type'] === 'application/json');
             localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
 
             return {

BIN
Web/src/assets/logo-mini.svg


BIN
Web/src/assets/logo.png


+ 4 - 1
Web/src/views/system/codeGen/component/editCodeGenDialog.vue

@@ -81,9 +81,10 @@
 						<el-form-item label="父级菜单" prop="menuPid">
 							<el-cascader
 								:options="state.menuData"
-								:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'title' }"
+								:props="cascaderProps"
 								placeholder="请选择上级菜单"
 								:disabled="!state.ruleForm.generateMenu"
+								filterable
 								clearable
 								class="w100"
 								v-model="state.ruleForm.menuPid"
@@ -172,6 +173,8 @@ const state = reactive({
 	printTypeList: [] as any,
 	printList: [] as Array<SysPrint>,
 });
+// 级联选择器配置选项
+const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'title' };
 
 onMounted(async () => {
 	var resDb = await getAPI(SysCodeGenApi).apiSysCodeGenDatabaseListGet();

+ 6 - 0
Web/src/views/system/database/component/genSeedData.vue

@@ -27,6 +27,11 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="过滤重复数据" prop="filterExistingData">
+							<el-switch v-model="state.ruleForm.filterExistingData"></el-switch>
+						</el-form-item>
+					</el-col>
 				</el-row>
 			</el-form>
 			<template #footer>
@@ -69,6 +74,7 @@ const openDialog = (row: any) => {
 	state.ruleForm.configId = row.configId;
 	state.ruleForm.tableName = row.tableName;
 	state.ruleForm.position = row.position;
+	state.ruleForm.filterExistingData = false;
 	state.isShowDialog = true;
 };
 

+ 3 - 8
Web/src/views/system/menu/component/editMenu.vue

@@ -11,14 +11,7 @@
 				<el-row :gutter="35">
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 						<el-form-item label="上级菜单">
-							<el-cascader
-								:options="props.menuData"
-								:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'title' }"
-								placeholder="请选择上级菜单"
-								clearable
-								class="w100"
-								v-model="state.ruleForm.pid"
-							>
+							<el-cascader :options="props.menuData" :props="cascaderProps" placeholder="请选择上级菜单" clearable filterable class="w100" v-model="state.ruleForm.pid">
 								<template #default="{ node, data }">
 									<span>{{ data.title }}</span>
 									<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
@@ -165,6 +158,8 @@ const state = reactive({
 	isShowDialog: false,
 	ruleForm: {} as UpdateMenuInput,
 });
+// 级联选择器配置选项
+const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'title' };
 
 // 获取全局组件大小
 const getGlobalComponentSize = computed(() => {

+ 3 - 8
Web/src/views/system/org/component/editOrg.vue

@@ -11,14 +11,7 @@
 				<el-row :gutter="35">
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 						<el-form-item label="上级机构">
-							<el-cascader
-								:options="props.orgData"
-								:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name' }"
-								placeholder="请选择上级机构"
-								clearable
-								class="w100"
-								v-model="state.ruleForm.pid"
-							>
+							<el-cascader :options="props.orgData" :props="cascaderProps" placeholder="请选择上级机构" clearable filterable class="w100" v-model="state.ruleForm.pid">
 								<template #default="{ node, data }">
 									<span>{{ data.name }}</span>
 									<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
@@ -96,6 +89,8 @@ const state = reactive({
 	ruleForm: {} as UpdateOrgInput,
 	orgTypeList: [] as any,
 });
+// 级联选择器配置选项
+const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name' };
 
 onMounted(async () => {
 	let resDicData = await getAPI(SysDictDataApi).apiSysDictDataDataListCodeGet('org_type');

+ 3 - 1
Web/src/views/system/region/component/editRegion.vue

@@ -13,7 +13,7 @@
 						<el-form-item label="上级名称">
 							<el-cascader
 								:options="regionData"
-								:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name' }"
+								:props="cascaderProps"
 								placeholder="请选择上级名称"
 								clearable
 								class="w100"
@@ -84,6 +84,8 @@ const state = reactive({
 	isShowDialog: false,
 	ruleForm: {} as UpdateRegionInput,
 });
+// 级联选择器配置选项
+//const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name' };
 
 // 打开弹窗
 const openDialog = (row: any) => {

+ 56 - 52
Web/src/views/system/server/index.vue

@@ -4,38 +4,40 @@
 			<el-col :md="12" :sm="24">
 				<el-card shadow="hover" header="系统信息">
 					<table class="sysInfo_table">
-						<tr>
-							<td class="sysInfo_td">主机名称:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.hostName }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">操作系统:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.systemOs }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">系统架构:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.osArchitecture }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">CPU核数:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.processorCount }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">运行时长:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.sysRunTime }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">外网地址:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.remoteIp }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">内网地址:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.localIp }}</td>
-						</tr>
-						<tr>
-							<td class="sysInfo_td">运行框架:</td>
-							<td class="sysInfo_td">{{ state.machineBaseInfo.frameworkDescription }}</td>
-						</tr>
+						<tbody>
+							<tr>
+								<td class="sysInfo_td">主机名称:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.hostName }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">操作系统:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.systemOs }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">系统架构:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.osArchitecture }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">CPU核数:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.processorCount }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">运行时长:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.sysRunTime }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">外网地址:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.remoteIp }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">内网地址:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.localIp }}</td>
+							</tr>
+							<tr>
+								<td class="sysInfo_td">运行框架:</td>
+								<td class="sysInfo_td">{{ state.machineBaseInfo.frameworkDescription }}</td>
+							</tr>
+						</tbody>
 					</table>
 				</el-card>
 			</el-col>
@@ -74,26 +76,28 @@
 
 					<el-row>
 						<table class="sysInfo_table">
-							<tr>
-								<td class="sysInfo_td">启动时间:</td>
-								<td class="sysInfo_td">{{ state.machineUseInfo.startTime }}</td>
-							</tr>
-							<tr>
-								<td class="sysInfo_td">运行时长:</td>
-								<td class="sysInfo_td">{{ state.machineUseInfo.runTime }}</td>
-							</tr>
-							<tr>
-								<td class="sysInfo_td">网站目录:</td>
-								<td class="sysInfo_td">{{ state.machineBaseInfo.wwwroot }}</td>
-							</tr>
-							<tr>
-								<td class="sysInfo_td">开发环境:</td>
-								<td class="sysInfo_td">{{ state.machineBaseInfo.environment }}</td>
-							</tr>
-							<tr>
-								<td class="sysInfo_td">环境变量:</td>
-								<td class="sysInfo_td">{{ state.machineBaseInfo.stage }}</td>
-							</tr>
+							<tbody>
+								<tr>
+									<td class="sysInfo_td">启动时间:</td>
+									<td class="sysInfo_td">{{ state.machineUseInfo.startTime }}</td>
+								</tr>
+								<tr>
+									<td class="sysInfo_td">运行时长:</td>
+									<td class="sysInfo_td">{{ state.machineUseInfo.runTime }}</td>
+								</tr>
+								<tr>
+									<td class="sysInfo_td">网站目录:</td>
+									<td class="sysInfo_td">{{ state.machineBaseInfo.wwwroot }}</td>
+								</tr>
+								<tr>
+									<td class="sysInfo_td">开发环境:</td>
+									<td class="sysInfo_td">{{ state.machineBaseInfo.environment }}</td>
+								</tr>
+								<tr>
+									<td class="sysInfo_td">环境变量:</td>
+									<td class="sysInfo_td">{{ state.machineBaseInfo.stage }}</td>
+								</tr>
+							</tbody>
 						</table>
 					</el-row>
 				</el-card>

+ 6 - 17
Web/src/views/system/user/component/editUser.vue

@@ -62,14 +62,7 @@
 							</el-divider>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 								<el-form-item label="所属机构" prop="orgId" :rules="[{ required: true, message: '所属机构不能为空', trigger: 'blur' }]">
-									<el-cascader
-										:options="props.orgData"
-										:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' }"
-										placeholder="所属机构"
-										clearable
-										class="w100"
-										v-model="state.ruleForm.orgId"
-									>
+									<el-cascader :options="props.orgData" :props="cascaderProps" placeholder="所属机构" clearable filterable class="w100" v-model="state.ruleForm.orgId">
 										<template #default="{ node, data }">
 											<span>{{ data.name }}</span>
 											<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
@@ -115,14 +108,7 @@
 													<el-button icon="ele-Delete" type="danger" circle plain size="small" @click="deleteExtOrgRow(k)" />
 													<span class="ml5">机构</span>
 												</template>
-												<el-cascader
-													:options="props.orgData"
-													:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' }"
-													placeholder="机构组织"
-													clearable
-													class="w100"
-													v-model="state.ruleForm.extOrgIdList[k].orgId"
-												>
+												<el-cascader :options="props.orgData" :props="cascaderProps" placeholder="机构组织" clearable filterable class="w100" v-model="state.ruleForm.extOrgIdList[k].orgId">
 													<template #default="{ node, data }">
 														<span>{{ data.name }}</span>
 														<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
@@ -173,7 +159,8 @@
 									<el-radio-group v-model="state.ruleForm.sex">
 										<el-radio :value="1">男</el-radio>
 										<el-radio :value="2">女</el-radio>
-										<el-radio :value="3">其他</el-radio>
+										<el-radio :value="0">未知</el-radio>
+										<el-radio :value="9">未说明</el-radio>
 									</el-radio-group>
 								</el-form-item>
 							</el-col>
@@ -284,6 +271,8 @@ const state = reactive({
 	posData: [] as Array<SysPos>, // 职位数据
 	roleData: [] as Array<RoleOutput>, // 角色数据
 });
+// 级联选择器配置选项
+const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' };
 
 onMounted(async () => {
 	state.loading = true;

+ 15 - 10
Web/src/views/system/user/component/userCenter.vue

@@ -113,13 +113,13 @@
 						<el-tab-pane label="修改密码">
 							<el-form ref="ruleFormPasswordRef" :model="state.ruleFormPassword" label-width="auto">
 								<el-form-item label="当前密码" prop="passwordOld" :rules="[{ required: true, message: '当前密码不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleFormPassword.passwordOld" type="password" autocomplete="off" />
+									<el-input v-model="state.ruleFormPassword.passwordOld" type="password" autocomplete="off" show-password />
 								</el-form-item>
 								<el-form-item label="新密码" prop="passwordNew" :rules="[{ required: true, message: '新密码不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleFormPassword.passwordNew" type="password" autocomplete="off" />
+									<el-input v-model="state.ruleFormPassword.passwordNew" type="password" autocomplete="off" show-password />
 								</el-form-item>
 								<el-form-item label="确认密码" prop="passwordNew2" :rules="[{ validator: validatePassword, required: true, trigger: 'blur' }]">
-									<el-input v-model="state.passwordNew2" type="password" autocomplete="off" />
+									<el-input v-model="state.passwordNew2" type="password" autocomplete="off" show-password />
 								</el-form-item>
 								<el-form-item>
 									<el-button icon="ele-Refresh" @click="resetPassword">重 置</el-button>
@@ -165,7 +165,7 @@ import { storeToRefs } from 'pinia';
 import { ElForm, ElMessageBox, genFileId } from 'element-plus';
 import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
 import { useUserInfo } from '/@/stores/userInfo';
-import { base64ToFile } from '/@/utils/base64Conver';
+import { base64ToFile, blobToFile } from '/@/utils/base64Conver';
 import OrgTree from '/@/views/system/user/component/orgTree.vue';
 import CropperDialog from '/@/components/cropper/index.vue';
 import VueGridLayout from 'vue-grid-layout';
@@ -214,8 +214,9 @@ watch(state.signOptions, () => {
 
 // 上传头像图片
 const uploadCropperImg = async (e: any) => {
-	var res = await getAPI(SysFileApi).apiSysFileUploadAvatarPostForm(e.img);
+	var res = await getAPI(SysFileApi).apiSysFileUploadAvatarPostForm(blobToFile(e.img, userInfos.value.account + '.png'));
 	userInfos.value.avatar = getFileUrl(res.data.result!);
+	state.ruleFormBase.avatar = userInfos.value.avatar;
 };
 
 // 打开电子签名页面
@@ -226,11 +227,14 @@ const openSignDialog = () => {
 // 保存并上传电子签名
 const saveUploadSign = async () => {
 	const { isEmpty, data } = signaturePadRef.value.saveSignature();
-	if (isEmpty) return;
-
-	var res = await getAPI(SysFileApi).apiSysFileUploadSignaturePostForm(base64ToFile(data, userInfos.value.account + '.png'));
-	userInfos.value.signature = getFileUrl(res.data.result!);
-
+	if (isEmpty) {
+		userInfos.value.signature = null;
+		state.ruleFormBase.signature = null;
+	} else {
+		var res = await getAPI(SysFileApi).apiSysFileUploadSignaturePostForm(base64ToFile(data, userInfos.value.account + '.png'));
+		userInfos.value.signature = getFileUrl(res.data.result!);
+		state.ruleFormBase.signature = userInfos.value.signature;
+	}
 	clearSign();
 	state.signDialogVisible = false;
 };
@@ -249,6 +253,7 @@ const clearSign = () => {
 const uploadSignFile = async (file: any) => {
 	var res = await getAPI(SysFileApi).apiSysFileUploadSignaturePostForm(file.raw);
 	userInfos.value.signature = res.data.result?.url;
+	state.ruleFormBase.signature = userInfos.value.signature;
 };
 
 // 获得电子签名文件列表

+ 4 - 3
Web/src/views/system/user/index.vue

@@ -50,9 +50,10 @@
 						</el-table-column>
 						<el-table-column label="性别" width="70" align="center" show-overflow-tooltip>
 							<template #default="scope">
-								<el-tag type="success" v-if="scope.row.sex === 1"> 男 </el-tag>
-								<el-tag type="danger" v-else-if="scope.row.sex === 2"> 女 </el-tag>
-								<el-tag type="info" v-else> 其他 </el-tag>
+								<el-tag v-if="scope.row.sex === 1" type="success">男</el-tag>
+								<el-tag v-else-if="scope.row.sex === 2" type="danger">女</el-tag>
+								<el-tag v-else-if="scope.row.sex === 0" type="info">未知</el-tag>
+								<el-tag v-else-if="scope.row.sex === 9" type="info">未说明</el-tag>
 							</template>
 						</el-table-column> -->
 						<el-table-column label="账号类型" width="110" align="center" show-overflow-tooltip>

+ 24 - 5
Web/src/views/system/weChatPay/index.vue

@@ -67,7 +67,13 @@
 			/>
 		</el-card>
 
-		<el-dialog v-model="showAddDialog" title="新增模拟数据">
+		<el-dialog v-model="showAddDialog">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
+					<span>新增模拟数据</span>
+				</div>
+			</template>
 			<el-form>
 				<el-form-item label="商品">
 					<el-input v-model="addData.description" placeholder="必填" clearable />
@@ -86,11 +92,23 @@
 				</span>
 			</template>
 		</el-dialog>
-		<el-dialog title="付款二维码" v-model="showQrDialog">
+		<el-dialog v-model="showQrDialog">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-View /> </el-icon>
+					<span>付款二维码</span>
+				</div>
+			</template>
 			<div ref="qrDiv"></div>
 		</el-dialog>
 
-		<el-dialog title="退款信息" v-model="showRefundDialog">
+		<el-dialog v-model="showRefundDialog">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Document /> </el-icon>
+					<span>退款信息</span>
+				</div>
+			</template>
 			<el-table :data="subTableData" style="width: 100%" tooltip-effect="light" row-key="id" border="">
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="outRefundNumber" label="商户退款号" width="180"></el-table-column>
@@ -117,6 +135,7 @@ import { ref, nextTick, onMounted, reactive } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import QRCode from 'qrcodejs2-fixes';
 import { pagePayList, createPay, getRefundListByID, refundDomestic } from '/@/api/system/weChatPay';
+import { SysWechatPay } from '/@/api-services/models';
 
 const qrDiv = ref<HTMLElement | null>(null);
 const showAddDialog = ref(false);
@@ -128,7 +147,7 @@ const addData = ref<any>({});
 
 const state = reactive({
 	loading: false,
-	tableData: [] as any,
+	tableData: [] as Array<SysWechatPay>,
 	queryParams: {
 		searchKey: undefined,
 		createTimeRange: undefined,
@@ -152,7 +171,7 @@ const handleQuery = async () => {
 	let params = Object.assign(state.queryParams, state.tableParams);
 	var res = await pagePayList(params);
 	let tmpRows = res.data.result?.items ?? [];
-	state.tableData.value = tmpRows;
+	state.tableData = tmpRows;
 	state.tableParams.total = res.data.result?.total;
 	state.loading = false;
 };

+ 1 - 1
Web/vite.config.ts

@@ -95,7 +95,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
 				...(JSON.parse(env.VITE_OPEN_CDN) ? { external: buildConfig.external } : {}),
 			},
 		},
-		css: { preprocessorOptions: { css: { charset: false } } },
+		css: { preprocessorOptions: { css: { charset: false }, scss: { silenceDeprecations: ['legacy-js-api'] } } },
 		define: {
 			__VUE_I18N_LEGACY_API__: JSON.stringify(false),
 			__VUE_I18N_FULL_INSTALL__: JSON.stringify(false),