浏览代码

Merge branch 'v2' of https://gitee.com/zuohuaijun/Admin.NET into v2

Cyrus Zhou 10 月之前
父节点
当前提交
ae8e95f915
共有 56 个文件被更改,包括 1277 次插入575 次删除
  1. 1 0
      Admin.NET/Admin.NET.Application/Configuration/Database.json
  2. 67 0
      Admin.NET/Admin.NET.Application/Entity/TestViewSysUser.cs
  3. 24 18
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  4. 59 0
      Admin.NET/Admin.NET.Core/Attribute/DataMaskAttribute.cs
  5. 1 1
      Admin.NET/Admin.NET.Core/Entity/EntityBase.cs
  6. 2 1
      Admin.NET/Admin.NET.Core/Entity/SysCodeGen.cs
  7. 1 1
      Admin.NET/Admin.NET.Core/Entity/SysRegion.cs
  8. 3 3
      Admin.NET/Admin.NET.Core/Entity/SysTenant.cs
  9. 1 1
      Admin.NET/Admin.NET.Core/Entity/SysTenantConfigData.cs
  10. 1 1
      Admin.NET/Admin.NET.Core/Entity/SysUserConfigData.cs
  11. 46 2
      Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs
  12. 1 2
      Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs
  13. 33 0
      Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs
  14. 7 1
      Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs
  15. 1 1
      Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs
  16. 1 1
      Admin.NET/Admin.NET.Core/Service/Config/SysTenantConfigService.cs
  17. 1 1
      Admin.NET/Admin.NET.Core/Service/Config/SysUserConfigService.cs
  18. 1 1
      Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs
  19. 18 11
      Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs
  20. 42 35
      Admin.NET/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs
  21. 6 1
      Admin.NET/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs
  22. 1 1
      Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs
  23. 5 2
      Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs
  24. 7 10
      Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs
  25. 5 2
      Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs
  26. 1 1
      Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs
  27. 187 187
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs
  28. 0 2
      Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs
  29. 192 192
      Admin.NET/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs
  30. 20 0
      Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarView.cs
  31. 30 0
      Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs
  32. 54 3
      Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs
  33. 240 0
      Admin.NET/Admin.NET.Core/Update/AutoVersionUpdate.cs
  34. 1 0
      Admin.NET/Admin.NET.Core/Utils/ComputerUtil.cs
  35. 4 3
      Admin.NET/Admin.NET.Core/Utils/FileHelper.cs
  36. 5 5
      Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj
  37. 1 1
      Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj
  38. 11 2
      Admin.NET/Admin.NET.Web.Core/Startup.cs
  39. 6 0
      Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  40. 3 0
      Admin.NET/Admin.NET.Web.Entry/UpdateScripts/1.0.0.sql
  41. 6 0
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkRoleUser.cs
  42. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkRoleJob.cs
  43. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListOutput.cs
  44. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListResult.cs
  45. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleResult.cs
  46. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistOutput.cs
  47. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistResult.cs
  48. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleListInput.cs
  49. 7 1
      Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleSimplelistInput.cs
  50. 2 2
      Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj
  51. 1 0
      README.md
  52. 39 24
      Web/package.json
  53. 29 8
      Web/src/components/sysDict/sysDict.vue
  54. 1 1
      Web/src/views/login/index.vue
  55. 52 38
      Web/src/views/system/tenant/component/editTenant.vue
  56. 1 1
      Web/src/views/system/tenant/index.vue

+ 1 - 0
Admin.NET/Admin.NET.Application/Configuration/Database.json

@@ -28,6 +28,7 @@
         //],
         //],
         "DbSettings": {
         "DbSettings": {
           "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
           "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
+          "EnableInitView": true, // 启用视图初始化(若实体和视图没有变化建议关闭)
           "EnableDiffLog": false, // 启用库表差异日志
           "EnableDiffLog": false, // 启用库表差异日志
           "EnableUnderLine": false, // 启用驼峰转下划线
           "EnableUnderLine": false, // 启用驼峰转下划线
           "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密)
           "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密)

+ 67 - 0
Admin.NET/Admin.NET.Application/Entity/TestViewSysUser.cs

@@ -0,0 +1,67 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using SqlSugar;
+
+namespace Admin.NET.Application;
+
+/// <summary>
+/// 用户表视图(必须加IgnoreTable,防止被生成为表)
+/// </summary>
+[SugarTable(null, "用户表视图"), IgnoreTable]
+public class TestViewSysUser : EntityBase, ISqlSugarView
+{
+    /// <summary>
+    /// 账号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "账号")]
+    public virtual string Account { get; set; }
+
+    /// <summary>
+    /// 真实姓名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "真实姓名")]
+    public virtual string RealName { get; set; }
+
+    /// <summary>
+    /// 昵称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "昵称")]
+    public string? NickName { get; set; }
+
+    /// <summary>
+    /// 机构名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "机构名称")]
+    public string? OrgName { get; set; }
+
+    /// <summary>
+    /// 职位名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "职位名称")]
+    public string? PosName { get; set; }
+
+    /// <summary>
+    /// 查询实例
+    /// </summary>
+    /// <param name="db"></param>
+    /// <returns></returns>
+    public string GetQueryableSqlString(SqlSugarScopeProvider db)
+    {
+        return db.Queryable<SysUser>()
+            .LeftJoin<SysOrg>((u, a) => u.OrgId == a.Id)
+            .LeftJoin<SysPos>((u, a, b) => u.PosId == b.Id)
+            .Select((u, a, b) => new TestViewSysUser
+            {
+                Id = u.Id,
+                Account = u.Account,
+                RealName = u.RealName,
+                NickName = u.NickName,
+                OrgName = a.Name,
+                PosName = b.Name,
+            }).ToMappedSqlString();
+    }
+}

+ 24 - 18
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -14,24 +14,24 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
     <PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
-    <PackageReference Include="AlipaySDKNet.Standard" Version="4.9.511" />
+    <PackageReference Include="AlipaySDKNet.Standard" Version="4.9.585" />
     <PackageReference Include="AngleSharp" Version="1.3.0" />
     <PackageReference Include="AngleSharp" Version="1.3.0" />
     <PackageReference Include="AspectCore.Extensions.Reflection" Version="2.4.0" />
     <PackageReference Include="AspectCore.Extensions.Reflection" Version="2.4.0" />
     <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
     <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
-    <PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.1" />
-    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.59" />
-    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.59" />
-    <PackageReference Include="Furion.Pure" Version="4.9.7.59" />
+    <PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.4" />
+    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.75" />
+    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.75" />
+    <PackageReference Include="Furion.Pure" Version="4.9.7.75" />
 	<PackageReference Include="Hardware.Info" Version="101.0.1" />
 	<PackageReference Include="Hardware.Info" Version="101.0.1" />
     <PackageReference Include="Hashids.net" Version="1.7.0" />
     <PackageReference Include="Hashids.net" Version="1.7.0" />
     <PackageReference Include="IPTools.China" Version="1.6.0" />
     <PackageReference Include="IPTools.China" Version="1.6.0" />
     <PackageReference Include="IPTools.International" Version="1.6.0" />
     <PackageReference Include="IPTools.International" Version="1.6.0" />
-    <PackageReference Include="log4net" Version="3.0.4" />
+    <PackageReference Include="log4net" Version="3.1.0" />
     <PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.2" />
     <PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.2" />
     <PackageReference Include="Magicodes.IE.Pdf" 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="Magicodes.IE.Word" Version="2.7.5.2" />
-    <PackageReference Include="MailKit" Version="4.12.0" />
-    <PackageReference Include="MiniExcel" Version="1.41.1" />
+    <PackageReference Include="MailKit" Version="4.12.1" />
+    <PackageReference Include="MiniExcel" Version="1.41.2" />
     <PackageReference Include="MiniWord" Version="0.9.2" />
     <PackageReference Include="MiniWord" Version="0.9.2" />
     <PackageReference Include="NewLife.Redis" Version="6.2.2025.503" />
     <PackageReference Include="NewLife.Redis" Version="6.2.2025.503" />
     <PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="4.0.0" />
     <PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="4.0.0" />
@@ -39,17 +39,17 @@
     <PackageReference Include="QRCoder" Version="1.6.0" />
     <PackageReference Include="QRCoder" Version="1.6.0" />
     <PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
     <PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
     <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
     <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
-    <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.8.0" />
+    <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.9.0" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.12.0" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.12.0" />
-    <PackageReference Include="SqlSugarCore" Version="5.1.4.190-preview30" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.4.194-preview24" />
     <PackageReference Include="SSH.NET" Version="2025.0.0" />
     <PackageReference Include="SSH.NET" Version="2025.0.0" />
-    <PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
+    <PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.4" />
     <PackageReference Include="System.Net.Http" Version="4.3.4" />
     <PackageReference Include="System.Net.Http" Version="4.3.4" />
     <PackageReference Include="System.Private.Uri" Version="4.3.2" />
     <PackageReference Include="System.Private.Uri" Version="4.3.2" />
-    <PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1232" />
+    <PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1246" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
-    <PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" Aliases="BouncyCastleV2" />
+    <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" Aliases="BouncyCastleV2" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
   <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
@@ -63,13 +63,19 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
   <ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
-    <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="9.3.0" />
-    <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="9.3.0" />
+    <PackageReference Include="AspNet.Security.OAuth.Gitee" Version="9.4.0" />
+    <PackageReference Include="AspNet.Security.OAuth.Weixin" Version="9.4.0" />
     <PackageReference Include="Lazy.Captcha.Core" Version="2.1.0" />
     <PackageReference Include="Lazy.Captcha.Core" Version="2.1.0" />
-    <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="9.0.4" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.4" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.4" />
+    <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="9.0.5" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.5" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.5" />
 	<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.5.1" />
 	<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.5.1" />
   </ItemGroup>
   </ItemGroup>
 
 
+  <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
+    <PackageReference Include="XiHan.Framework.Utils">
+      <Version>0.8.33</Version>
+    </PackageReference>
+  </ItemGroup>
+
 </Project>
 </Project>

+ 59 - 0
Admin.NET/Admin.NET.Core/Attribute/DataMaskAttribute.cs

@@ -0,0 +1,59 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 数据脱敏特性(支持自定义脱敏位置和脱敏字符)
+/// </summary>
+[AttributeUsage(AttributeTargets.Property)]
+public class DataMaskAttribute : Attribute
+{
+    /// <summary>
+    /// 脱敏起始位置(从0开始)
+    /// </summary>
+    private int StartIndex { get; }
+
+    /// <summary>
+    /// 脱敏长度
+    /// </summary>
+    private int Length { get; }
+
+    /// <summary>
+    /// 脱敏字符(默认*)
+    /// </summary>
+    private char MaskChar { get; set; } = '*';
+
+    /// <summary>
+    /// 是否保留原始长度(默认true)
+    /// </summary>
+    private bool KeepLength { get; set; } = true;
+
+    public DataMaskAttribute(int startIndex, int length)
+    {
+        if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex));
+        if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length));
+        
+        StartIndex = startIndex;
+        Length = length;
+    }
+
+    /// <summary>
+    /// 执行脱敏处理
+    /// </summary>
+    public string Mask(string input)
+    {
+        if (string.IsNullOrEmpty(input) || input.Length <= StartIndex)
+            return input;
+
+        var maskedLength = Math.Min(Length, input.Length - StartIndex);
+        var maskStr = new string(MaskChar, KeepLength ? maskedLength : Math.Min(4, maskedLength));
+
+        return input.Substring(0, StartIndex) + maskStr + 
+               (StartIndex + maskedLength < input.Length ? 
+                   input.Substring(StartIndex + maskedLength) : "");
+    }
+}

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

@@ -186,7 +186,7 @@ public abstract class EntityBaseTenantOrg : EntityBaseOrg, ITenantIdFilter
 /// <summary>
 /// <summary>
 /// 租户机构实体基类(数据权限、删除标志)
 /// 租户机构实体基类(数据权限、删除标志)
 /// </summary>
 /// </summary>
-public abstract class EntityBaseTenantOrgDel: EntityBaseOrgDel, ITenantIdFilter
+public abstract class EntityBaseTenantOrgDel : EntityBaseOrgDel, ITenantIdFilter
 {
 {
     /// <summary>
     /// <summary>
     /// 租户Id
     /// 租户Id

+ 2 - 1
Admin.NET/Admin.NET.Core/Entity/SysCodeGen.cs

@@ -42,6 +42,7 @@ public partial class SysCodeGen : EntityBase
     [SugarColumn(ColumnDescription = "库定位器名", Length = 64)]
     [SugarColumn(ColumnDescription = "库定位器名", Length = 64)]
     [MaxLength(64)]
     [MaxLength(64)]
     public string? ConfigId { get; set; }
     public string? ConfigId { get; set; }
+
     /// <summary>
     /// <summary>
     /// 库名
     /// 库名
     /// </summary>
     /// </summary>
@@ -62,6 +63,7 @@ public partial class SysCodeGen : EntityBase
             }
             }
         }
         }
     }
     }
+
     /// <summary>
     /// <summary>
     /// 数据库名(保留字段)
     /// 数据库名(保留字段)
     /// </summary>
     /// </summary>
@@ -154,5 +156,4 @@ public partial class SysCodeGen : EntityBase
     /// </summary>
     /// </summary>
     [SugarColumn(IsIgnore = true)]
     [SugarColumn(IsIgnore = true)]
     public virtual List<TableUniqueConfigItem> TableUniqueList => string.IsNullOrWhiteSpace(TableUniqueConfig) ? null : JSON.Deserialize<List<TableUniqueConfigItem>>(TableUniqueConfig);
     public virtual List<TableUniqueConfigItem> TableUniqueList => string.IsNullOrWhiteSpace(TableUniqueConfig) ? null : JSON.Deserialize<List<TableUniqueConfigItem>>(TableUniqueConfig);
-
 }
 }

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysRegion.cs

@@ -12,7 +12,7 @@ namespace Admin.NET.Core;
 [SugarTable(null, "系统行政地区表")]
 [SugarTable(null, "系统行政地区表")]
 [SysTable]
 [SysTable]
 [SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)]
 [SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)]
-[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)]
+[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)]
 public partial class SysRegion : EntityBaseId
 public partial class SysRegion : EntityBaseId
 {
 {
     /// <summary>
     /// <summary>

+ 3 - 3
Admin.NET/Admin.NET.Core/Entity/SysTenant.cs

@@ -79,20 +79,20 @@ public partial class SysTenant : EntityBase
     /// <summary>
     /// <summary>
     /// 图标
     /// 图标
     /// </summary>
     /// </summary>
-    [SugarColumn(ColumnDescription = "图标", Length = 256), Required, MaxLength(256)]
+    [SugarColumn(ColumnDescription = "图标", Length = 256), MaxLength(256)]
     public virtual string? Logo { get; set; }
     public virtual string? Logo { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 标题
     /// 标题
     /// </summary>
     /// </summary>
     [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)]
     [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)]
-    public virtual string Title { get; set; }
+    public virtual string? Title { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 副标题
     /// 副标题
     /// </summary>
     /// </summary>
     [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)]
     [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)]
-    public virtual string ViceTitle { get; set; }
+    public virtual string? ViceTitle { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 副描述
     /// 副描述

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysTenantConfigData.cs

@@ -26,4 +26,4 @@ public class SysTenantConfigData : EntityBaseTenantId
     [SugarColumn(ColumnDescription = "参数值", Length = 512)]
     [SugarColumn(ColumnDescription = "参数值", Length = 512)]
     [MaxLength(512)]
     [MaxLength(512)]
     public string? Value { get; set; }
     public string? Value { get; set; }
-}
+}

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysUserConfigData.cs

@@ -32,4 +32,4 @@ public class SysUserConfigData : EntityBaseId
     [SugarColumn(ColumnDescription = "参数值", Length = 512)]
     [SugarColumn(ColumnDescription = "参数值", Length = 512)]
     [MaxLength(512)]
     [MaxLength(512)]
     public string? Value { get; set; }
     public string? Value { get; set; }
-}
+}

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

@@ -5,7 +5,6 @@
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
 
 using Newtonsoft.Json;
 using Newtonsoft.Json;
-using System.Text.Json;
 
 
 namespace Admin.NET.Core;
 namespace Admin.NET.Core;
 
 
@@ -15,6 +14,16 @@ namespace Admin.NET.Core;
 [SuppressSniffer]
 [SuppressSniffer]
 public static partial class ObjectExtension
 public static partial class ObjectExtension
 {
 {
+    /// <summary>
+    /// 类型属性列表映射表
+    /// </summary>
+    private static readonly ConcurrentDictionary<Type, PropertyInfo[]> PropertyCache = new();
+
+    /// <summary>
+    /// 脱敏特性缓存映射表
+    /// </summary>
+    private static readonly ConcurrentDictionary<PropertyInfo, DataMaskAttribute> AttributeCache = new();
+
     /// <summary>
     /// <summary>
     /// 判断类型是否实现某个泛型
     /// 判断类型是否实现某个泛型
     /// </summary>
     /// </summary>
@@ -70,7 +79,7 @@ public static partial class ObjectExtension
     public static string ToJson(this object obj)
     public static string ToJson(this object obj)
     {
     {
         var jsonSettings = SetNewtonsoftJsonSetting();
         var jsonSettings = SetNewtonsoftJsonSetting();
-        return JSON.GetJsonSerializer().Serialize(obj,jsonSettings);
+        return JSON.GetJsonSerializer().Serialize(obj, jsonSettings);
     }
     }
 
 
     private static JsonSerializerSettings SetNewtonsoftJsonSetting()
     private static JsonSerializerSettings SetNewtonsoftJsonSetting()
@@ -474,4 +483,39 @@ public static partial class ObjectExtension
         var json = JSON.Serialize(obj, jsonSettings);
         var json = JSON.Serialize(obj, jsonSettings);
         return JSON.Deserialize<T>(json);
         return JSON.Deserialize<T>(json);
     }
     }
+
+    /// <summary>
+    /// 对带有<see cref="DataMaskAttribute"/>特性字段进行脱敏处理
+    /// </summary>
+    public static T MaskSensitiveData<T>(this T obj) where T : class
+    {
+        if (obj == null) return null;
+
+        var type = typeof(T);
+    
+        // 获取或缓存属性集合
+        var properties = PropertyCache.GetOrAdd(type, t => 
+            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+                .Where(p => p.PropertyType == typeof(string) && p.GetCustomAttribute<DataMaskAttribute>() != null)
+                .ToArray());
+
+        // 并行处理可写属性
+        Parallel.ForEach(properties, prop =>
+        {
+            if (!prop.CanWrite) return;
+        
+            // 获取或缓存特性
+            var maskAttr = AttributeCache.GetOrAdd(prop, p => p.GetCustomAttribute<DataMaskAttribute>());
+            
+            if (maskAttr == null) return;
+
+            // 处理非空字符串
+            if (prop.GetValue(obj) is string { Length: > 0 } value)
+            {
+                prop.SetValue(obj, maskAttr.Mask(value));
+            }
+        });
+
+        return obj;
+    }
 }
 }

+ 1 - 2
Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs

@@ -399,9 +399,8 @@ public static class RepositoryExtension
             //判断是否是相同属性
             //判断是否是相同属性
             PropertyInfo pro = t.GetType().GetProperty(info.Name);
             PropertyInfo pro = t.GetType().GetProperty(info.Name);
             var attr = pro.GetCustomAttribute<SugarColumn>();
             var attr = pro.GetCustomAttribute<SugarColumn>();
-            if (pro != null && attr!=null &&!attr.IsPrimaryKey)
+            if (pro != null && attr != null && !attr.IsPrimaryKey)
                 updateColumns.Add(info.Name);
                 updateColumns.Add(info.Name);
-
         }
         }
         uNOption.UpdateColumns = updateColumns.ToArray();
         uNOption.UpdateColumns = updateColumns.ToArray();
         return uNOption;
         return uNOption;

+ 33 - 0
Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs

@@ -291,4 +291,37 @@ public static class SqlSugarExtension
 
 
         return Expression.Call(selectorExpr, method, constant);
         return Expression.Call(selectorExpr, method, constant);
     }
     }
+    
+    #region 视图操作
+
+    /// <summary>
+    /// 获取映射SQL语句, 用于创建视图
+    /// </summary>
+    /// <param name="queryable"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static string ToMappedSqlString<T>(this ISugarQueryable<T> queryable) where T : class
+    {
+        ArgumentNullException.ThrowIfNull(queryable);
+
+        // 获取实体映射信息
+        var entityInfo = queryable.Context.EntityMaintenance.GetEntityInfo(typeof(T));
+        if (entityInfo?.Columns == null || entityInfo.Columns.Count == 0) return queryable.ToSqlString();
+
+        // 构建需要替换的字段名映射(只处理实际有差异的字段)
+        var nameMap = entityInfo.Columns
+            .Where(c => !string.Equals(c.PropertyName, c.DbColumnName, StringComparison.OrdinalIgnoreCase))
+            .ToDictionary(k => k.PropertyName.ToLower(), v => v.DbColumnName, StringComparer.OrdinalIgnoreCase);
+        if (nameMap.Count == 0) return queryable.ToSqlString();
+
+        // 预编译正则表达式提升性能
+        var sql = queryable.ToSqlString();
+        foreach (var kv in nameMap)
+        {
+            sql = Regex.Replace(sql, $@"\b{kv.Key}\b", kv.Value ?? kv.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled); // 单词边界匹配
+        }
+        return sql;
+    }
+
+    #endregion 视图操作
 }
 }

+ 7 - 1
Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs

@@ -45,6 +45,7 @@ public sealed class DbConnectionConfig : ConnectionConfig
     /// 数据库名称
     /// 数据库名称
     /// </summary>
     /// </summary>
     public string DbNickName { get; set; }
     public string DbNickName { get; set; }
+
     /// <summary>
     /// <summary>
     /// 数据库配置
     /// 数据库配置
     /// </summary>
     /// </summary>
@@ -68,7 +69,7 @@ public sealed class DbConnectionConfig : ConnectionConfig
     /// <summary>
     /// <summary>
     /// 数据库存储目录(仅SqlServer支持指定目录创建)
     /// 数据库存储目录(仅SqlServer支持指定目录创建)
     /// </summary>
     /// </summary>
-    public string DatabaseDirectory { get; set; }    
+    public string DatabaseDirectory { get; set; }
 }
 }
 
 
 /// <summary>
 /// <summary>
@@ -80,6 +81,11 @@ public sealed class DbSettings
     /// 启用库表初始化
     /// 启用库表初始化
     /// </summary>
     /// </summary>
     public bool EnableInitDb { get; set; }
     public bool EnableInitDb { get; set; }
+    
+    /// <summary>
+    /// 启用视图初始化
+    /// </summary>
+    public bool EnableInitView { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 启用库表差异日志
     /// 启用库表差异日志

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

@@ -51,7 +51,7 @@ public class SysCodeGenService : IDynamicApiController, ITransient
         return await _db.Queryable<SysCodeGen>()
         return await _db.Queryable<SysCodeGen>()
             .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
-            .ToPagedListAsync(input.Page, input.PageSize); 
+            .ToPagedListAsync(input.Page, input.PageSize);
     }
     }
 
 
     /// <summary>
     /// <summary>

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

@@ -28,7 +28,7 @@ public class SysTenantConfigService : IDynamicApiController, ITransient
         _sysConfigRep = sysConfigRep;
         _sysConfigRep = sysConfigRep;
         _sysConfigDataRep = sysConfigDataRep;
         _sysConfigDataRep = sysConfigDataRep;
         VSysConfig = _sysConfigRep.AsQueryable().LeftJoin(_sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.TenantId == _userManager.TenantId),
         VSysConfig = _sysConfigRep.AsQueryable().LeftJoin(_sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.TenantId == _userManager.TenantId),
-            (c, cv) => c.Id == cv.ConfigId).MergeTable().Select<SysConfig>();
+            (c, cv) => c.Id == cv.ConfigId).Select<SysConfig>().MergeTable();
     }
     }
 
 
     /// <summary>
     /// <summary>

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

@@ -28,7 +28,7 @@ public class SysUserConfigService : IDynamicApiController, ITransient
         _sysConfigRep = sysConfigRep;
         _sysConfigRep = sysConfigRep;
         _sysConfigDataRep = sysConfigDataRep;
         _sysConfigDataRep = sysConfigDataRep;
         VSysConfig = _sysConfigRep.AsQueryable().LeftJoin(_sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.UserId == _userManager.UserId),
         VSysConfig = _sysConfigRep.AsQueryable().LeftJoin(_sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.UserId == _userManager.UserId),
-            (c, cv) => c.Id == cv.ConfigId).MergeTable().Select<SysConfig>();
+            (c, cv) => c.Id == cv.ConfigId).Select<SysConfig>().MergeTable();
     }
     }
 
 
     /// <summary>
     /// <summary>

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

@@ -10,8 +10,8 @@ public class VisualDb
 {
 {
     public string ConfigId { get; set; }
     public string ConfigId { get; set; }
     public string DbNickName { get; set; }
     public string DbNickName { get; set; }
-
 }
 }
+
 /// <summary>
 /// <summary>
 /// 库表可视化
 /// 库表可视化
 /// </summary>
 /// </summary>

+ 18 - 11
Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs

@@ -188,6 +188,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
             throw Oops.Oh(ErrorCodeEnum.D3011);
             throw Oops.Oh(ErrorCodeEnum.D3011);
 
 
         var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable()
         var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable()
+            .Where(u => u.Status == StatusEnum.Enable)
             .WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code)
             .WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code)
             .WhereIF(typeId != null, u => u.Id == typeId)
             .WhereIF(typeId != null, u => u.Id == typeId)
             .FirstAsync();
             .FirstAsync();
@@ -198,18 +199,24 @@ public class SysDictDataService : IDynamicApiController, ITransient
         if (dictDataList == null)
         if (dictDataList == null)
         {
         {
             //平台字典和租户字典分开缓存
             //平台字典和租户字典分开缓存
-            ISugarQueryable<SysDictData> sysDictData = _sysDictDataRep.AsQueryable();
             if (dictType.IsTenant == YesNoEnum.Y)
             if (dictType.IsTenant == YesNoEnum.Y)
-                sysDictData = _sysDictDataRep.Change<SysDictDataTenant>().AsQueryable().WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId).Select<SysDictData>();
-
-            dictDataList = await sysDictData.InnerJoin<SysDictType>((u, a) => u.DictTypeId == a.Id)
-                .Where(u => u.DictTypeId == dictType.Id)
-                .Select((u, a) => new SysDictData
-                {
-                    Status = u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable,
-                }, true)
-                .OrderBy(u => new { u.OrderNo, u.Value, u.Code })
-                .ToListAsync();
+            {
+                dictDataList = await _sysDictDataRep.Change<SysDictDataTenant>().AsQueryable()
+                       .Where(u => u.DictTypeId == dictType.Id)
+                       .Where(u => u.Status == StatusEnum.Enable)
+                       .WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId).Select<SysDictData>()
+                       .OrderBy(u => new { u.OrderNo, u.Value, u.Code })
+                       .ToListAsync();
+            }
+            else
+            {
+                dictDataList = await _sysDictDataRep.AsQueryable()
+                    .Where(u => u.DictTypeId == dictType.Id)
+                    .Where(u => u.Status == StatusEnum.Enable)
+                    .OrderBy(u => new { u.OrderNo, u.Value, u.Code })
+                    .ToListAsync();
+            }
+
             _sysCacheService.Set(dicKey, dictDataList);
             _sysCacheService.Set(dicKey, dictDataList);
         }
         }
         return dictDataList;
         return dictDataList;

+ 42 - 35
Admin.NET/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs

@@ -1,3 +1,8 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
 
 /*
 /*
  *━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  *━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -25,12 +30,12 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
     private readonly IHttpRemoteService _httpRemoteService;
     private readonly IHttpRemoteService _httpRemoteService;
 
 
     /// <summary>
     /// <summary>
-    /// 百度翻译appId  
+    /// 百度翻译appId
     /// </summary>
     /// </summary>
     private static readonly string _appId = "xxxxxxxxxxx";
     private static readonly string _appId = "xxxxxxxxxxx";
 
 
     /// <summary>
     /// <summary>
-    /// 百度翻译appKey  
+    /// 百度翻译appKey
     /// </summary>
     /// </summary>
     private static readonly string _appKey = "xxxxxxxxxxx";
     private static readonly string _appKey = "xxxxxxxxxxx";
 
 
@@ -42,15 +47,26 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
     // 语言映射字典
     // 语言映射字典
     private static readonly Dictionary<string, string> langMap = new Dictionary<string, string>
     private static readonly Dictionary<string, string> langMap = new Dictionary<string, string>
     {
     {
-        ["en"] = "en", ["de"]     = "de", ["fi"]     = "fin",
-        ["es"] = "spa", ["fr"]    = "fra", ["it"]    = "it",
-        ["ja"] = "jp", ["ko"]     = "kor", ["no"]    = "nor",
-        ["pl"] = "pl", ["pt"]     = "pt", ["ru"]     = "ru",
-        ["th"] = "th", ["id"]     = "id", ["ms"]     = "may",
-        ["vi"] = "vie", ["zh-HK"] = "yue", ["zh-TW"] = "cht"
+        ["en"] = "en",
+        ["de"] = "de",
+        ["fi"] = "fin",
+        ["es"] = "spa",
+        ["fr"] = "fra",
+        ["it"] = "it",
+        ["ja"] = "jp",
+        ["ko"] = "kor",
+        ["no"] = "nor",
+        ["pl"] = "pl",
+        ["pt"] = "pt",
+        ["ru"] = "ru",
+        ["th"] = "th",
+        ["id"] = "id",
+        ["ms"] = "may",
+        ["vi"] = "vie",
+        ["zh-HK"] = "yue",
+        ["zh-TW"] = "cht"
     };
     };
 
 
-
     /// <summary>
     /// <summary>
     /// 初始化一个<see cref="BaiDuTranslationService"/>类型的新实例.
     /// 初始化一个<see cref="BaiDuTranslationService"/>类型的新实例.
     /// </summary>
     /// </summary>
@@ -60,7 +76,6 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
         _httpRemoteService = httpRemoteService;
         _httpRemoteService = httpRemoteService;
     }
     }
 
 
-
     /// <summary>
     /// <summary>
     /// 百度在线翻译
     /// 百度在线翻译
     /// </summary>
     /// </summary>
@@ -94,17 +109,16 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
     /// <returns>翻译后的文本内容</returns>
     /// <returns>翻译后的文本内容</returns>
     [DisplayName("百度在线翻译")]
     [DisplayName("百度在线翻译")]
     [HttpGet]
     [HttpGet]
-    public async Task<BaiDuTranslationResult> Translation([FromQuery] [Required] string from, [FromQuery] [Required] string to, [FromQuery] [Required] string content)
+    public async Task<BaiDuTranslationResult> Translation([FromQuery][Required] string from, [FromQuery][Required] string to, [FromQuery][Required] string content)
     {
     {
         // 标准版API授权只能翻译基础18语言,201种需要企业尊享版支持见百度api 文档
         // 标准版API授权只能翻译基础18语言,201种需要企业尊享版支持见百度api 文档
-        Random rd   = new Random();
+        Random rd = new Random();
         string salt = rd.Next(100000).ToString();
         string salt = rd.Next(100000).ToString();
         // 改成您的密钥
         // 改成您的密钥
         string secretKey = _appKey;
         string secretKey = _appKey;
-        string sign      = EncryptString(_appId + content + salt + secretKey);
-        string url       = $"{_baseUrl}q={HttpUtility.UrlEncode(content)}&from={from}&to={to}&appid={_appId}&salt={salt}&sign={sign}";
-        var    res       = await _httpRemoteService.GetAsAsync<BaiDuTranslationResult>(url);
-
+        string sign = EncryptString(_appId + content + salt + secretKey);
+        string url = $"{_baseUrl}q={HttpUtility.UrlEncode(content)}&from={from}&to={to}&appid={_appId}&salt={salt}&sign={sign}";
+        var res = await _httpRemoteService.GetAsAsync<BaiDuTranslationResult>(url);
 
 
         if (!res.error_code.Equals("0"))
         if (!res.error_code.Equals("0"))
         {
         {
@@ -114,7 +128,6 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
         return res;
         return res;
     }
     }
 
 
-
 #if DEBUG
 #if DEBUG
 
 
     /// <summary>
     /// <summary>
@@ -150,8 +163,7 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
             foreach (var file in files)
             foreach (var file in files)
             {
             {
                 var langCode = Path.GetFileNameWithoutExtension(file);
                 var langCode = Path.GetFileNameWithoutExtension(file);
-                var langDic  = await ReadLanguageFile(file);
-
+                var langDic = await ReadLanguageFile(file);
 
 
                 // 查询出没有生成的键值对
                 // 查询出没有生成的键值对
 
 
@@ -159,7 +171,7 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
                 // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
                 // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
                 // 转换为 HashSet 提升性能
                 // 转换为 HashSet 提升性能
                 var langDicKey = new HashSet<string>(langDic.Keys);
                 var langDicKey = new HashSet<string>(langDic.Keys);
-                var notGen     = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
+                var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
 
 
                 // 没有未生成的跳出
                 // 没有未生成的跳出
                 if (notGen.Count == 0)
                 if (notGen.Count == 0)
@@ -181,7 +193,6 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
 
 
                         var result = await Translation("zh", targetLang, $"{gen.Value}");
                         var result = await Translation("zh", targetLang, $"{gen.Value}");
 
 
-
                         if (!result.error_code.Equals("0"))
                         if (!result.error_code.Equals("0"))
                         {
                         {
                             continue;
                             continue;
@@ -201,7 +212,7 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
                         {
                         {
                             translationValue = translationValue.Replace("'", "\\'");
                             translationValue = translationValue.Replace("'", "\\'");
                         }
                         }
-                        
+
                         str += ($"        {gen.Key}: '{translationValue}',{Environment.NewLine}");
                         str += ($"        {gen.Key}: '{translationValue}',{Environment.NewLine}");
                     }
                     }
                     catch (Exception e)
                     catch (Exception e)
@@ -223,7 +234,6 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
         }
         }
     }
     }
 
 
-
     /// <summary>
     /// <summary>
     /// 生成前端菜单i18n文件
     /// 生成前端菜单i18n文件
     /// </summary>
     /// </summary>
@@ -257,7 +267,7 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
             foreach (var file in files)
             foreach (var file in files)
             {
             {
                 var langCode = Path.GetFileNameWithoutExtension(file);
                 var langCode = Path.GetFileNameWithoutExtension(file);
-                var langDic  = await ReadLanguageFile(file);
+                var langDic = await ReadLanguageFile(file);
 
 
                 // 查询出没有生成的键值对
                 // 查询出没有生成的键值对
 
 
@@ -265,7 +275,7 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
                 // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
                 // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
                 // 转换为 HashSet 提升性能
                 // 转换为 HashSet 提升性能
                 var langDicKey = new HashSet<string>(langDic.Keys);
                 var langDicKey = new HashSet<string>(langDic.Keys);
-                var notGen     = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
+                var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
 
 
                 // 没有未生成的跳出
                 // 没有未生成的跳出
                 if (notGen.Count == 0)
                 if (notGen.Count == 0)
@@ -287,7 +297,6 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
 
 
                         var result = await Translation("zh", targetLang, $"{gen.Value}");
                         var result = await Translation("zh", targetLang, $"{gen.Value}");
 
 
-                        
                         if (!result.error_code.Equals("0"))
                         if (!result.error_code.Equals("0"))
                         {
                         {
                             continue;
                             continue;
@@ -339,15 +348,15 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
             throw Oops.Bah("【zh-CN.ts】文件未找到");
             throw Oops.Bah("【zh-CN.ts】文件未找到");
         }
         }
 
 
-        var       dic    = new Dictionary<string, string>();
+        var dic = new Dictionary<string, string>();
         using var reader = new StreamReader(baseFile, Encoding.UTF8);
         using var reader = new StreamReader(baseFile, Encoding.UTF8);
 
 
         while (await reader.ReadLineAsync() is { } line)
         while (await reader.ReadLineAsync() is { } line)
         {
         {
             if (line.Contains('{') || line.Contains('}')) continue;
             if (line.Contains('{') || line.Contains('}')) continue;
 
 
-            var cleanLine                               = line.Trim().TrimEnd(',').Replace("'", "");
-            var parts                                   = cleanLine.Split(new[] { ':' }, 2);
+            var cleanLine = line.Trim().TrimEnd(',').Replace("'", "");
+            var parts = cleanLine.Split(new[] { ':' }, 2);
             if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
             if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
         }
         }
 
 
@@ -362,15 +371,15 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
             throw Oops.Bah($"【{filePath.Split('/').Last()}】文件未找到");
             throw Oops.Bah($"【{filePath.Split('/').Last()}】文件未找到");
         }
         }
 
 
-        var       dic    = new Dictionary<string, string>();
+        var dic = new Dictionary<string, string>();
         using var reader = new StreamReader(filePath, Encoding.UTF8);
         using var reader = new StreamReader(filePath, Encoding.UTF8);
 
 
         while (await reader.ReadLineAsync() is { } line)
         while (await reader.ReadLineAsync() is { } line)
         {
         {
             if (line.Contains('{') || line.Contains('}')) continue;
             if (line.Contains('{') || line.Contains('}')) continue;
 
 
-            var cleanLine                               = line.Trim().TrimEnd(',').Replace("'", "");
-            var parts                                   = cleanLine.Split(new[] { ':' }, 2);
+            var cleanLine = line.Trim().TrimEnd(',').Replace("'", "");
+            var parts = cleanLine.Split(new[] { ':' }, 2);
             if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
             if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
         }
         }
 
 
@@ -392,12 +401,10 @@ public class BaiDuTranslationService : IDynamicApiController, ITransient
         Console.ResetColor();
         Console.ResetColor();
     }
     }
 
 
-    #endregion
-
+    #endregion 辅助方法
 
 
 #endif
 #endif
 
 
-
     // 计算MD5值
     // 计算MD5值
     [NonAction]
     [NonAction]
     private static string EncryptString(string str)
     private static string EncryptString(string str)

+ 6 - 1
Admin.NET/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs

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

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

@@ -375,7 +375,7 @@ public class SysJobService : IDynamicApiController, ITransient
     {
     {
         _sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable<SysJobTriggerRecord>();
         _sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable<SysJobTriggerRecord>();
     }
     }
-    
+
     /// <summary>
     /// <summary>
     /// 清空不保留的作业触发器运行记录 🔖
     /// 清空不保留的作业触发器运行记录 🔖
     /// </summary>
     /// </summary>

+ 5 - 2
Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs

@@ -288,7 +288,6 @@ public class SysRegionService : IDynamicApiController, ITransient
                 Level = 1,
                 Level = 1,
                 Pid = 0,
                 Pid = 0,
             };
             };
-            if (municipalityList.Any(m => province.Name.StartsWith(m))) province.Name += "(省)";
             list.Add(province);
             list.Add(province);
 
 
             if (syncLevel <= 1) continue;
             if (syncLevel <= 1) continue;
@@ -306,7 +305,11 @@ public class SysRegionService : IDynamicApiController, ITransient
                     Name = prefName,
                     Name = prefName,
                     Level = 2
                     Level = 2
                 };
                 };
-                if (municipalityList.Any(m => city.Name.StartsWith(m))) city.Name += "(地)";
+                if (municipalityList.Any(m => city.Name.StartsWith(m)))
+                {
+                    city.Name = "市辖区";
+                    if (province.Code == city.Code) city.Code = province.Code.Substring(0, 2) + "0100";
+                }
                 list.Add(city);
                 list.Add(city);
 
 
                 if (syncLevel <= 2) continue;
                 if (syncLevel <= 2) continue;

+ 7 - 10
Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs

@@ -1,4 +1,4 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
 //
 //
 // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
 // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
 //
 //
@@ -44,45 +44,42 @@ public class AddTenantInput : TenantOutput
     /// <summary>
     /// <summary>
     /// 系统主标题
     /// 系统主标题
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "系统主标题不能为空")]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Title)", "系统主标题不能为空")]
     public override string Title { get; set; }
     public override string Title { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 系统副标题
     /// 系统副标题
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "系统副标题不能为空")]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceTitle)", "系统副标题不能为空")]
     public override string ViceTitle { get; set; }
     public override string ViceTitle { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 系统描述
     /// 系统描述
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "系统描述不能为空")]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceDesc)", "系统描述不能为空")]
     public override string ViceDesc { get; set; }
     public override string ViceDesc { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// 版权说明
     /// 版权说明
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "版权说明不能为空")]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Copyright)", "版权说明不能为空")]
     public override string Copyright { get; set; }
     public override string Copyright { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// ICP备案号
     /// ICP备案号
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "ICP备案号不能为空")]
     public override string Icp { get; set; }
     public override string Icp { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// ICP地址
     /// ICP地址
     /// </summary>
     /// </summary>
-    [Required(ErrorMessage = "ICP地址不能为空")]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && !string.IsNullOrWhiteSpace(Icp) && string.IsNullOrWhiteSpace(IcpUrl)", "ICP地址不能为空")]
     public override string IcpUrl { get; set; }
     public override string IcpUrl { get; set; }
 
 
     /// <summary>
     /// <summary>
     /// Logo图片Base64码
     /// Logo图片Base64码
     /// </summary>
     /// </summary>
-    [CommonValidation(
-        "string.IsNullOrWhiteSpace(Logo) && string.IsNullOrWhiteSpace(LogoBase64)", "图标不能为空"
-    )]
+    [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Logo) && string.IsNullOrWhiteSpace(LogoBase64)", "图标不能为空")]
     public virtual string LogoBase64 { get; set; }
     public virtual string LogoBase64 { get; set; }
 
 
     /// <summary>
     /// <summary>

+ 5 - 2
Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs

@@ -66,8 +66,8 @@ public class SysTenantService : IDynamicApiController, ITransient
     public async Task<SqlSugarPagedList<TenantOutput>> Page(PageTenantInput input)
     public async Task<SqlSugarPagedList<TenantOutput>> Page(PageTenantInput input)
     {
     {
         return await _sysTenantRep.AsQueryable()
         return await _sysTenantRep.AsQueryable()
-            .LeftJoin<SysUser>((u, a) => u.UserId == a.Id)
-            .LeftJoin<SysOrg>((u, a, b) => u.OrgId == b.Id)
+            .LeftJoin<SysUser>((u, a) => u.UserId == a.Id).ClearFilter()
+            .LeftJoin<SysOrg>((u, a, b) => u.OrgId == b.Id).ClearFilter()
             .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), (u, a) => a.Phone.Contains(input.Phone.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), (u, a) => a.Phone.Contains(input.Phone.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (u, a, b) => b.Name.Contains(input.Name.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (u, a, b) => b.Name.Contains(input.Name.Trim()))
             .OrderBy(u => new { u.OrderNo, u.Id })
             .OrderBy(u => new { u.OrderNo, u.Id })
@@ -201,6 +201,8 @@ public class SysTenantService : IDynamicApiController, ITransient
     [NonAction]
     [NonAction]
     public void SetLogoUrl(SysTenant tenant, string logoBase64, string logoFileName)
     public void SetLogoUrl(SysTenant tenant, string logoBase64, string logoFileName)
     {
     {
+        if (string.IsNullOrEmpty(tenant?.Logo) && string.IsNullOrEmpty(tenant?.Logo)) return;
+
         // 旧图标文件相对路径
         // 旧图标文件相对路径
         var oldSysLogoRelativeFilePath = tenant.Logo ?? "";
         var oldSysLogoRelativeFilePath = tenant.Logo ?? "";
         var oldSysLogoAbsoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, oldSysLogoRelativeFilePath.TrimStart('/'));
         var oldSysLogoAbsoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, oldSysLogoRelativeFilePath.TrimStart('/'));
@@ -354,6 +356,7 @@ public class SysTenantService : IDynamicApiController, ITransient
     /// 获取种子数据类型
     /// 获取种子数据类型
     /// </summary>
     /// </summary>
     /// <param name="config">数据库连接配置</param>
     /// <param name="config">数据库连接配置</param>
+    /// <param name="typeName"></param>
     /// <returns>种子数据类型列表</returns>
     /// <returns>种子数据类型列表</returns>
     [NonAction]
     [NonAction]
     private List<Type> GetSeedDataTypes(DbConnectionConfig config, string typeName)
     private List<Type> GetSeedDataTypes(DbConnectionConfig config, string typeName)

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

@@ -144,7 +144,7 @@ public class GenerateQRImageInput
 public class GenerateQRImageUnLimitInput : GenerateQRImageInput
 public class GenerateQRImageUnLimitInput : GenerateQRImageInput
 {
 {
     /// <summary>
     /// <summary>
-    /// 二维码携带的参数 eg:a=1(最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~)
+    /// 二维码携带的参数 eg:a=1(最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:<!-- !#$&'()*+,/:;=?@-._~ -->
     /// </summary>
     /// </summary>
     public string Scene { get; set; }
     public string Scene { get; set; }
 }
 }

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

@@ -12,191 +12,191 @@ namespace Admin.NET.Core.Service;
 [ApiDescriptionSettings(Order = 230)]
 [ApiDescriptionSettings(Order = 230)]
 public class SysWechatService : IDynamicApiController, ITransient
 public class SysWechatService : IDynamicApiController, ITransient
 {
 {
-	private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
-	private readonly SysConfigService _sysConfigService;
-	private readonly WechatApiClientFactory _wechatApiClientFactory;
-	private readonly WechatApiClient _wechatApiClient;
-
-	public SysWechatService(SqlSugarRepository<SysWechatUser> sysWechatUserRep,
-		SysConfigService sysConfigService,
-		WechatApiClientFactory wechatApiClientFactory,
-		SysCacheService sysCacheService)
-	{
-		_sysWechatUserRep = sysWechatUserRep;
-		_sysConfigService = sysConfigService;
-		_wechatApiClientFactory = wechatApiClientFactory;
-		_wechatApiClient = wechatApiClientFactory.CreateWechatClient();
-	}
-
-	/// <summary>
-	/// 生成网页授权Url 🔖
-	/// </summary>
-	/// <param name="input"></param>
-	/// <returns></returns>
-	[AllowAnonymous]
-	[DisplayName("生成网页授权Url")]
-	public string GenAuthUrl(GenAuthUrlInput input)
-	{
-		return _wechatApiClient.GenerateParameterizedUrlForConnectOAuth2Authorize(input.RedirectUrl, input.Scope, input.State);
-	}
-
-	/// <summary>
-	/// 获取微信用户OpenId 🔖
-	/// </summary>
-	/// <param name="input"></param>
-	[AllowAnonymous]
-	[DisplayName("获取微信用户OpenId")]
-	public async Task<string> SnsOAuth2([FromQuery] WechatOAuth2Input input)
-	{
-		var reqOAuth2 = new SnsOAuth2AccessTokenRequest()
-		{
-			Code = input.Code,
-		};
-		var resOAuth2 = await _wechatApiClient.ExecuteSnsOAuth2AccessTokenAsync(reqOAuth2);
-		if (resOAuth2.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-			throw Oops.Oh(resOAuth2.ErrorMessage + " " + resOAuth2.ErrorCode);
-
-		var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resOAuth2.OpenId);
-		if (wxUser == null)
-		{
-			var reqUserInfo = new SnsUserInfoRequest()
-			{
-				OpenId = resOAuth2.OpenId,
-				AccessToken = resOAuth2.AccessToken,
-			};
-			var resUserInfo = await _wechatApiClient.ExecuteSnsUserInfoAsync(reqUserInfo);
-			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
-		{
-			wxUser.AccessToken = resOAuth2.AccessToken;
-			wxUser.RefreshToken = resOAuth2.RefreshToken;
-			await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
-		}
-
-		return resOAuth2.OpenId;
-	}
-
-	/// <summary>
-	/// 微信用户登录OpenId 🔖
-	/// </summary>
-	/// <param name="input"></param>
-	/// <returns></returns>
-	[AllowAnonymous]
-	[DisplayName("微信用户登录OpenId")]
-	public async Task<dynamic> OpenIdLogin(WechatUserLogin input)
-	{
-		var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId);
-		if (wxUser == null)
-			throw Oops.Oh("微信用户登录OpenId错误");
-
-		var tokenExpire = await _sysConfigService.GetTokenExpire();
-		return new
-		{
-			wxUser.Avatar,
-			accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
-			{
-				{ ClaimConst.UserId, wxUser.Id },
-				{ ClaimConst.NickName, wxUser.NickName },
-				{ ClaimConst.LoginMode, LoginModeEnum.APP },
-			}, tokenExpire)
-		};
-	}
-
-	/// <summary>
-	/// 获取配置签名参数(wx.config) 🔖
-	/// </summary>
-	/// <returns></returns>
-	[DisplayName("获取配置签名参数(wx.config)")]
-	public async Task<dynamic> GenConfigPara(SignatureInput input)
-	{
-		string ticket = await _wechatApiClientFactory.TryGetWechatJsApiTicketAsync();
-		return _wechatApiClient.GenerateParametersForJSSDKConfig(ticket, input.Url);
-	}
-
-	/// <summary>
-	/// 获取模板列表 🔖
-	/// </summary>
-	[DisplayName("获取模板列表")]
-	public async Task<dynamic> GetMessageTemplateList()
-	{
-		var accessToken = await GetCgibinToken();
-		var reqTemplate = new CgibinTemplateGetAllPrivateTemplateRequest()
-		{
-			AccessToken = accessToken
-		};
-		var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateGetAllPrivateTemplateAsync(reqTemplate);
-		if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-			throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode);
-
-		return resTemplate.TemplateList;
-	}
-
-	/// <summary>
-	/// 发送模板消息 🔖
-	/// </summary>
-	/// <param name="input"></param>
-	/// <returns></returns>
-	[DisplayName("发送模板消息")]
-	public async Task<dynamic> SendTemplateMessage(MessageTemplateSendInput input)
-	{
-		var dataInfo = input.Data.ToDictionary(k => k.Key, k => k.Value);
-		var messageData = new Dictionary<string, CgibinMessageTemplateSendRequest.Types.DataItem>();
-		foreach (var item in dataInfo)
-		{
-			messageData.Add(item.Key, new CgibinMessageTemplateSendRequest.Types.DataItem() { Value = "" + item.Value.Value.ToString() + "" });
-		}
-
-		var accessToken = await GetCgibinToken();
-		var reqMessage = new CgibinMessageTemplateSendRequest()
-		{
-			AccessToken = accessToken,
-			TemplateId = input.TemplateId,
-			ToUserOpenId = input.ToUserOpenId,
-			Url = input.Url,
-			MiniProgram = new CgibinMessageTemplateSendRequest.Types.MiniProgram
-			{
-				AppId = _wechatApiClientFactory._wechatOptions.WxOpenAppId,
-				PagePath = input.MiniProgramPagePath,
-			},
-			Data = messageData
-		};
-		var resMessage = await _wechatApiClient.ExecuteCgibinMessageTemplateSendAsync(reqMessage);
-		return resMessage;
-	}
-
-	/// <summary>
-	/// 删除模板 🔖
-	/// </summary>
-	/// <param name="input"></param>
-	/// <returns></returns>
-	[ApiDescriptionSettings(Name = "DeleteMessageTemplate"), HttpPost]
-	[DisplayName("删除模板")]
-	public async Task<dynamic> DeleteMessageTemplate(DeleteMessageTemplateInput input)
-	{
-		var accessToken = await GetCgibinToken();
-		var reqMessage = new CgibinTemplateDeletePrivateTemplateRequest()
-		{
-			AccessToken = accessToken,
-			TemplateId = input.TemplateId
-		};
-		var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateDeletePrivateTemplateAsync(reqMessage);
-		return resTemplate;
-	}
-
-	/// <summary>
-	/// 获取Access_token
-	/// </summary>
-	[NonAction]
-	public async Task<string> GetCgibinToken()
-	{
-		return await _wechatApiClientFactory.TryGetWechatAccessTokenAsync();
-	}
+    private readonly SqlSugarRepository<SysWechatUser> _sysWechatUserRep;
+    private readonly SysConfigService _sysConfigService;
+    private readonly WechatApiClientFactory _wechatApiClientFactory;
+    private readonly WechatApiClient _wechatApiClient;
+
+    public SysWechatService(SqlSugarRepository<SysWechatUser> sysWechatUserRep,
+        SysConfigService sysConfigService,
+        WechatApiClientFactory wechatApiClientFactory,
+        SysCacheService sysCacheService)
+    {
+        _sysWechatUserRep = sysWechatUserRep;
+        _sysConfigService = sysConfigService;
+        _wechatApiClientFactory = wechatApiClientFactory;
+        _wechatApiClient = wechatApiClientFactory.CreateWechatClient();
+    }
+
+    /// <summary>
+    /// 生成网页授权Url 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("生成网页授权Url")]
+    public string GenAuthUrl(GenAuthUrlInput input)
+    {
+        return _wechatApiClient.GenerateParameterizedUrlForConnectOAuth2Authorize(input.RedirectUrl, input.Scope, input.State);
+    }
+
+    /// <summary>
+    /// 获取微信用户OpenId 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    [AllowAnonymous]
+    [DisplayName("获取微信用户OpenId")]
+    public async Task<string> SnsOAuth2([FromQuery] WechatOAuth2Input input)
+    {
+        var reqOAuth2 = new SnsOAuth2AccessTokenRequest()
+        {
+            Code = input.Code,
+        };
+        var resOAuth2 = await _wechatApiClient.ExecuteSnsOAuth2AccessTokenAsync(reqOAuth2);
+        if (resOAuth2.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+            throw Oops.Oh(resOAuth2.ErrorMessage + " " + resOAuth2.ErrorCode);
+
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resOAuth2.OpenId);
+        if (wxUser == null)
+        {
+            var reqUserInfo = new SnsUserInfoRequest()
+            {
+                OpenId = resOAuth2.OpenId,
+                AccessToken = resOAuth2.AccessToken,
+            };
+            var resUserInfo = await _wechatApiClient.ExecuteSnsUserInfoAsync(reqUserInfo);
+            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
+        {
+            wxUser.AccessToken = resOAuth2.AccessToken;
+            wxUser.RefreshToken = resOAuth2.RefreshToken;
+            await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync();
+        }
+
+        return resOAuth2.OpenId;
+    }
+
+    /// <summary>
+    /// 微信用户登录OpenId 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("微信用户登录OpenId")]
+    public async Task<dynamic> OpenIdLogin(WechatUserLogin input)
+    {
+        var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId);
+        if (wxUser == null)
+            throw Oops.Oh("微信用户登录OpenId错误");
+
+        var tokenExpire = await _sysConfigService.GetTokenExpire();
+        return new
+        {
+            wxUser.Avatar,
+            accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
+            {
+                { ClaimConst.UserId, wxUser.Id },
+                { ClaimConst.NickName, wxUser.NickName },
+                { ClaimConst.LoginMode, LoginModeEnum.APP },
+            }, tokenExpire)
+        };
+    }
+
+    /// <summary>
+    /// 获取配置签名参数(wx.config) 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取配置签名参数(wx.config)")]
+    public async Task<dynamic> GenConfigPara(SignatureInput input)
+    {
+        string ticket = await _wechatApiClientFactory.TryGetWechatJsApiTicketAsync();
+        return _wechatApiClient.GenerateParametersForJSSDKConfig(ticket, input.Url);
+    }
+
+    /// <summary>
+    /// 获取模板列表 🔖
+    /// </summary>
+    [DisplayName("获取模板列表")]
+    public async Task<dynamic> GetMessageTemplateList()
+    {
+        var accessToken = await GetCgibinToken();
+        var reqTemplate = new CgibinTemplateGetAllPrivateTemplateRequest()
+        {
+            AccessToken = accessToken
+        };
+        var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateGetAllPrivateTemplateAsync(reqTemplate);
+        if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+            throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode);
+
+        return resTemplate.TemplateList;
+    }
+
+    /// <summary>
+    /// 发送模板消息 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("发送模板消息")]
+    public async Task<dynamic> SendTemplateMessage(MessageTemplateSendInput input)
+    {
+        var dataInfo = input.Data.ToDictionary(k => k.Key, k => k.Value);
+        var messageData = new Dictionary<string, CgibinMessageTemplateSendRequest.Types.DataItem>();
+        foreach (var item in dataInfo)
+        {
+            messageData.Add(item.Key, new CgibinMessageTemplateSendRequest.Types.DataItem() { Value = "" + item.Value.Value.ToString() + "" });
+        }
+
+        var accessToken = await GetCgibinToken();
+        var reqMessage = new CgibinMessageTemplateSendRequest()
+        {
+            AccessToken = accessToken,
+            TemplateId = input.TemplateId,
+            ToUserOpenId = input.ToUserOpenId,
+            Url = input.Url,
+            MiniProgram = new CgibinMessageTemplateSendRequest.Types.MiniProgram
+            {
+                AppId = _wechatApiClientFactory._wechatOptions.WxOpenAppId,
+                PagePath = input.MiniProgramPagePath,
+            },
+            Data = messageData
+        };
+        var resMessage = await _wechatApiClient.ExecuteCgibinMessageTemplateSendAsync(reqMessage);
+        return resMessage;
+    }
+
+    /// <summary>
+    /// 删除模板 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "DeleteMessageTemplate"), HttpPost]
+    [DisplayName("删除模板")]
+    public async Task<dynamic> DeleteMessageTemplate(DeleteMessageTemplateInput input)
+    {
+        var accessToken = await GetCgibinToken();
+        var reqMessage = new CgibinTemplateDeletePrivateTemplateRequest()
+        {
+            AccessToken = accessToken,
+            TemplateId = input.TemplateId
+        };
+        var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateDeletePrivateTemplateAsync(reqMessage);
+        return resTemplate;
+    }
+
+    /// <summary>
+    /// 获取Access_token
+    /// </summary>
+    [NonAction]
+    public async Task<string> GetCgibinToken()
+    {
+        return await _wechatApiClientFactory.TryGetWechatAccessTokenAsync();
+    }
 }
 }

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

@@ -353,8 +353,6 @@ public class SysWxOpenService : IDynamicApiController, ITransient
             AccessToken = accessToken,
             AccessToken = accessToken,
             Width = input.Width,
             Width = input.Width,
             PagePath = input.PagePath,
             PagePath = input.PagePath,
-
-
         };
         };
         var response = await _wechatApiClient.ExecuteWxaGetWxaCodeAsync(request);
         var response = await _wechatApiClient.ExecuteWxaGetWxaCodeAsync(request);
 
 

+ 192 - 192
Admin.NET/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs

@@ -13,196 +13,196 @@ namespace Admin.NET.Core.Service;
 /// </summary>
 /// </summary>
 public partial class WechatApiClientFactory : ISingleton
 public partial class WechatApiClientFactory : ISingleton
 {
 {
-	private readonly IHttpClientFactory _httpClientFactory;
-	public readonly WechatOptions _wechatOptions;
-	private readonly SysCacheService _sysCacheService;
-
-	public WechatApiClientFactory(IHttpClientFactory httpClientFactory, IOptions<WechatOptions> wechatOptions, SysCacheService sysCacheService)
-	{
-		_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
-		_wechatOptions = wechatOptions.Value ?? throw new ArgumentNullException(nameof(wechatOptions));
-		_sysCacheService = sysCacheService;
-	}
-
-	/// <summary>
-	/// 微信公众号
-	/// </summary>
-	/// <returns></returns>
-	public WechatApiClient CreateWechatClient()
-	{
-		if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret))
-			throw Oops.Oh("微信公众号配置错误");
-
-		var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
-		{
-			AppId = _wechatOptions.WechatAppId,
-			AppSecret = _wechatOptions.WechatAppSecret,
-			PushToken = _wechatOptions.WechatToken,
-			PushEncodingAESKey = _wechatOptions.WechatEncodingAESKey,
-		})
-		.UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
-		.Build();
-
-		client.Configure(config =>
-		{
-			JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
-			jsonSerializerSettings.Formatting = Formatting.Indented;
-			config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
-																						  // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json  JSON序列化
-		});
-
-		return client;
-	}
-
-	/// <summary>
-	/// 微信小程序
-	/// </summary>
-	/// <returns></returns>
-	public WechatApiClient CreateWxOpenClient()
-	{
-		if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret))
-			throw Oops.Oh("微信小程序配置错误");
-
-		var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
-		{
-			AppId = _wechatOptions.WxOpenAppId,
-			AppSecret = _wechatOptions.WxOpenAppSecret,
-			PushToken = _wechatOptions.WxToken,
-			PushEncodingAESKey = _wechatOptions.WxEncodingAESKey,
-		})
-		.UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
-		.Build();
-
-		client.Configure(config =>
-		{
-			JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
-			jsonSerializerSettings.Formatting = Formatting.Indented;
-			config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
-																						  // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json  JSON序列化
-		});
-
-		return client;
-	}
-
-	/// <summary>
-	/// 获取微信公众号AccessToken
-	/// </summary>
-	/// <returns></returns>
-	public async Task<string> TryGetWechatAccessTokenAsync()
-	{
-		if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}")))
-		{
-			var client = CreateWechatClient();
-			var reqCgibinToken = new CgibinTokenRequest();
-			var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
-			if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-				throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
-			_sysCacheService.Set($"WxAccessToken_{_wechatOptions.WechatAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
-		}
-
-		return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}");
-	}
-
-	/// <summary>
-	/// 获取微信小程序AccessToken
-	/// </summary>
-	/// <returns></returns>
-	public async Task<string> TryGetWxOpenAccessTokenAsync()
-	{
-		if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WxOpenAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}")))
-		{
-			var client = CreateWxOpenClient();
-			var reqCgibinToken = new CgibinTokenRequest();
-			var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
-			if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-				throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
-			_sysCacheService.Set($"WxAccessToken_{_wechatOptions.WxOpenAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
-		}
-
-		return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
-	}
-
-	/// <summary>
-	/// 检查微信公众号AccessToken
-	/// </summary>
-	/// <returns></returns>
-	public async Task CheckWechatAccessTokenAsync()
-	{
-		if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) return;
-
-		var req = new CgibinOpenApiQuotaGetRequest
-		{
-			AccessToken = await TryGetWechatAccessTokenAsync(),
-			CgiPath = "/cgi-bin/token"
-		};
-		var client = CreateWechatClient();
-		var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
-
-		var originColor = Console.ForegroundColor;
-		if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-		{
-			_sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WechatAppId}");
-			Console.ForegroundColor = ConsoleColor.Red;
-			Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 无效");
-		}
-		else
-		{
-			Console.ForegroundColor = ConsoleColor.Magenta;
-			Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 有效");
-		}
-		Console.ForegroundColor = originColor;
-	}
-
-	/// <summary>
-	/// 检查微信小程序AccessToken
-	/// </summary>
-	/// <returns></returns>
-	public async Task CheckWxOpenAccessTokenAsync()
-	{
-		if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) return;
-
-		var req = new CgibinOpenApiQuotaGetRequest
-		{
-			AccessToken = await TryGetWxOpenAccessTokenAsync(),
-			CgiPath = "/cgi-bin/token"
-		};
-		var client = CreateWxOpenClient();
-		var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
-
-		var originColor = Console.ForegroundColor;
-		if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
-		{
-			_sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
-			Console.ForegroundColor = ConsoleColor.Red;
-			Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 无效");
-		}
-		else
-		{
-			Console.ForegroundColor = ConsoleColor.Magenta;
-			Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 有效");
-		}
-		Console.ForegroundColor = originColor;
-	}
-
-	/// <summary>
-	/// 获取微信JS接口临时票据jsapi_ticket
-	/// </summary>
-	/// <returns></returns>
-	public async Task<string> TryGetWechatJsApiTicketAsync()
-	{
-		if (!_sysCacheService.ExistKey($"WxJsApiTicket_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}")))
-		{
-			var accessToken = await TryGetWechatAccessTokenAsync();
-			var client = CreateWechatClient();
-			var request = new CgibinTicketGetTicketRequest()
-			{
-				AccessToken = accessToken
-			};
-			var response = await client.ExecuteCgibinTicketGetTicketAsync(request);
-			if (!response.IsSuccessful())
-				throw Oops.Oh(response.ErrorMessage + " " + response.ErrorCode);
-			_sysCacheService.Set($"WxJsApiTicket_{_wechatOptions.WechatAppId}", response.Ticket, TimeSpan.FromSeconds(response.ExpiresIn - 60));
-		}
-		return _sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}");
-	}
+    private readonly IHttpClientFactory _httpClientFactory;
+    public readonly WechatOptions _wechatOptions;
+    private readonly SysCacheService _sysCacheService;
+
+    public WechatApiClientFactory(IHttpClientFactory httpClientFactory, IOptions<WechatOptions> wechatOptions, SysCacheService sysCacheService)
+    {
+        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
+        _wechatOptions = wechatOptions.Value ?? throw new ArgumentNullException(nameof(wechatOptions));
+        _sysCacheService = sysCacheService;
+    }
+
+    /// <summary>
+    /// 微信公众号
+    /// </summary>
+    /// <returns></returns>
+    public WechatApiClient CreateWechatClient()
+    {
+        if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret))
+            throw Oops.Oh("微信公众号配置错误");
+
+        var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
+        {
+            AppId = _wechatOptions.WechatAppId,
+            AppSecret = _wechatOptions.WechatAppSecret,
+            PushToken = _wechatOptions.WechatToken,
+            PushEncodingAESKey = _wechatOptions.WechatEncodingAESKey,
+        })
+        .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
+        .Build();
+
+        client.Configure(config =>
+        {
+            JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
+            jsonSerializerSettings.Formatting = Formatting.Indented;
+            config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
+                                                                                          // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json  JSON序列化
+        });
+
+        return client;
+    }
+
+    /// <summary>
+    /// 微信小程序
+    /// </summary>
+    /// <returns></returns>
+    public WechatApiClient CreateWxOpenClient()
+    {
+        if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret))
+            throw Oops.Oh("微信小程序配置错误");
+
+        var client = WechatApiClientBuilder.Create(new WechatApiClientOptions()
+        {
+            AppId = _wechatOptions.WxOpenAppId,
+            AppSecret = _wechatOptions.WxOpenAppSecret,
+            PushToken = _wechatOptions.WxToken,
+            PushEncodingAESKey = _wechatOptions.WxEncodingAESKey,
+        })
+        .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁
+        .Build();
+
+        client.Configure(config =>
+        {
+            JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings();
+            jsonSerializerSettings.Formatting = Formatting.Indented;
+            config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化
+                                                                                          // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json  JSON序列化
+        });
+
+        return client;
+    }
+
+    /// <summary>
+    /// 获取微信公众号AccessToken
+    /// </summary>
+    /// <returns></returns>
+    public async Task<string> TryGetWechatAccessTokenAsync()
+    {
+        if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}")))
+        {
+            var client = CreateWechatClient();
+            var reqCgibinToken = new CgibinTokenRequest();
+            var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
+            if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+                throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
+            _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WechatAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
+        }
+
+        return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WechatAppId}");
+    }
+
+    /// <summary>
+    /// 获取微信小程序AccessToken
+    /// </summary>
+    /// <returns></returns>
+    public async Task<string> TryGetWxOpenAccessTokenAsync()
+    {
+        if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WxOpenAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}")))
+        {
+            var client = CreateWxOpenClient();
+            var reqCgibinToken = new CgibinTokenRequest();
+            var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken);
+            if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+                throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode);
+            _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WxOpenAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60));
+        }
+
+        return _sysCacheService.Get<string>($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
+    }
+
+    /// <summary>
+    /// 检查微信公众号AccessToken
+    /// </summary>
+    /// <returns></returns>
+    public async Task CheckWechatAccessTokenAsync()
+    {
+        if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) return;
+
+        var req = new CgibinOpenApiQuotaGetRequest
+        {
+            AccessToken = await TryGetWechatAccessTokenAsync(),
+            CgiPath = "/cgi-bin/token"
+        };
+        var client = CreateWechatClient();
+        var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
+
+        var originColor = Console.ForegroundColor;
+        if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+        {
+            _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WechatAppId}");
+            Console.ForegroundColor = ConsoleColor.Red;
+            Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 无效");
+        }
+        else
+        {
+            Console.ForegroundColor = ConsoleColor.Magenta;
+            Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 有效");
+        }
+        Console.ForegroundColor = originColor;
+    }
+
+    /// <summary>
+    /// 检查微信小程序AccessToken
+    /// </summary>
+    /// <returns></returns>
+    public async Task CheckWxOpenAccessTokenAsync()
+    {
+        if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) return;
+
+        var req = new CgibinOpenApiQuotaGetRequest
+        {
+            AccessToken = await TryGetWxOpenAccessTokenAsync(),
+            CgiPath = "/cgi-bin/token"
+        };
+        var client = CreateWxOpenClient();
+        var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req);
+
+        var originColor = Console.ForegroundColor;
+        if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功)
+        {
+            _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WxOpenAppId}");
+            Console.ForegroundColor = ConsoleColor.Red;
+            Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 无效");
+        }
+        else
+        {
+            Console.ForegroundColor = ConsoleColor.Magenta;
+            Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 有效");
+        }
+        Console.ForegroundColor = originColor;
+    }
+
+    /// <summary>
+    /// 获取微信JS接口临时票据jsapi_ticket
+    /// </summary>
+    /// <returns></returns>
+    public async Task<string> TryGetWechatJsApiTicketAsync()
+    {
+        if (!_sysCacheService.ExistKey($"WxJsApiTicket_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}")))
+        {
+            var accessToken = await TryGetWechatAccessTokenAsync();
+            var client = CreateWechatClient();
+            var request = new CgibinTicketGetTicketRequest()
+            {
+                AccessToken = accessToken
+            };
+            var response = await client.ExecuteCgibinTicketGetTicketAsync(request);
+            if (!response.IsSuccessful())
+                throw Oops.Oh(response.ErrorMessage + " " + response.ErrorCode);
+            _sysCacheService.Set($"WxJsApiTicket_{_wechatOptions.WechatAppId}", response.Ticket, TimeSpan.FromSeconds(response.ExpiresIn - 60));
+        }
+        return _sysCacheService.Get<string>($"WxJsApiTicket_{_wechatOptions.WechatAppId}");
+    }
 }
 }

+ 20 - 0
Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarView.cs

@@ -0,0 +1,20 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 视图实体接口
+/// </summary>
+public interface ISqlSugarView
+{
+    /// <summary>
+    /// 获取视图查询sql语句
+    /// </summary>
+    /// <param name="db"></param>
+    /// <returns></returns>
+    public string GetQueryableSqlString(SqlSugarScopeProvider db);
+}

+ 30 - 0
Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs

@@ -113,6 +113,36 @@ public static class SqlSugarPagedExtensions
         return CreateSqlSugarPagedList(items, total, pageIndex, pageSize);
         return CreateSqlSugarPagedList(items, total, pageIndex, pageSize);
     }
     }
 
 
+    /// <summary>
+    /// 脱敏分页拓展
+    /// </summary>
+    /// <param name="query"><see cref="ISugarQueryable{TEntity}"/>对象</param>
+    /// <param name="pageIndex">当前页码,从1开始</param>
+    /// <param name="pageSize">页码容量</param>
+    /// <returns></returns>
+    public static async Task<SqlSugarPagedList<TEntity>> ToPagedListDataMaskAsync<TEntity>(this ISugarQueryable<TEntity> query, int pageIndex, int pageSize) where TEntity : class
+    {
+        RefAsync<int> total = 0;
+        var items = await query.ToPageListAsync(pageIndex, pageSize, total);
+        items.ForEach(x => x.MaskSensitiveData());
+        return CreateSqlSugarPagedList(items, total, pageIndex, pageSize);
+    }
+    
+    /// <summary>
+    /// 脱敏分页拓展
+    /// </summary>
+    /// <param name="list">集合对象</param>
+    /// <param name="pageIndex">当前页码,从1开始</param>
+    /// <param name="pageSize">页码容量</param>
+    /// <returns></returns>
+    public static SqlSugarPagedList<TEntity> ToPagedListDataMask<TEntity>(this IEnumerable<TEntity> list, int pageIndex, int pageSize) where TEntity : class
+    {
+        var total = list.Count();
+        var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
+        items.ForEach(x => x.MaskSensitiveData());
+        return CreateSqlSugarPagedList(items, total, pageIndex, pageSize);
+    }
+
     /// <summary>
     /// <summary>
     /// 分页拓展
     /// 分页拓展
     /// </summary>
     /// </summary>

+ 54 - 3
Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs

@@ -150,7 +150,6 @@ public static class SqlSugarSetup
                     Console.ForegroundColor = ConsoleColor.Red;
                     Console.ForegroundColor = ConsoleColor.Red;
                 Console.WriteLine(log);
                 Console.WriteLine(log);
                 Console.ForegroundColor = originColor;
                 Console.ForegroundColor = originColor;
-                App.PrintToMiniProfiler("SqlSugar", "Info", log);
             };
             };
         }
         }
         db.Aop.OnError = ex =>
         db.Aop.OnError = ex =>
@@ -158,7 +157,6 @@ public static class SqlSugarSetup
             if (ex.Parametres == null) return;
             if (ex.Parametres == null) return;
             var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parametres)}\r\n";
             var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parametres)}\r\n";
             Log.Error(log, ex);
             Log.Error(log, ex);
-            App.PrintToMiniProfiler("SqlSugar", "Error", log);
         };
         };
         db.Aop.OnLogExecuted = (sql, pars) =>
         db.Aop.OnLogExecuted = (sql, pars) =>
         {
         {
@@ -178,7 +176,6 @@ public static class SqlSugarSetup
             var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名
             var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名
             var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}";
             var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}";
             Log.Warning(log);
             Log.Warning(log);
-            App.PrintToMiniProfiler("SqlSugar", "Slow", log);
         };
         };
 
 
         // 数据审计
         // 数据审计
@@ -328,6 +325,48 @@ public static class SqlSugarSetup
         db.Aop.OnDiffLogEvent = AopOnDiffLogEvent;
         db.Aop.OnDiffLogEvent = AopOnDiffLogEvent;
     }
     }
 
 
+    /// <summary>
+    /// 初始化视图
+    /// </summary>
+    /// <param name="dbProvider"></param>
+    private static void InitView(SqlSugarScopeProvider dbProvider)
+    {
+        var totalWatch = Stopwatch.StartNew(); // 开始总计时
+        Log.Information($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId}");
+        var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList();
+
+        int taskIndex = 0, size = viewTypeList.Count;
+        var taskList = viewTypeList.Select(viewType => Task.Run(() =>
+        {
+            // 开始计时
+            var stopWatch = Stopwatch.StartNew();
+
+            // 获取视图实体和配置信息
+            var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误");
+
+            // 如果视图存在,则删除视图
+            if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName)))
+                dbProvider.DbMaintenance.DropView(entityInfo.DbTableName);
+
+            // 获取初始化视图查询SQL
+            var sql = viewType.GetMethod(nameof(ISqlSugarView.GetQueryableSqlString))?.Invoke(Activator.CreateInstance(viewType), [dbProvider]) as string;
+            if (string.IsNullOrWhiteSpace(sql)) throw new Exception("视图初始化Sql语句不能为空");
+
+            // 创建视图
+            dbProvider.Ado.ExecuteCommand($"CREATE VIEW {entityInfo.DbTableName} AS " + Environment.NewLine + " " + sql);
+
+            // 停止计时
+            stopWatch.Stop();
+            Console.ForegroundColor = ConsoleColor.Green;
+            Console.WriteLine($"初始化视图 {viewType.FullName,-58} ({dbProvider.CurrentConnectionConfig.ConfigId} - {Interlocked.Increment(ref taskIndex):D003}/{size:D003},耗时:{stopWatch.ElapsedMilliseconds:N0} ms)");
+        }));
+        Task.WaitAll(taskList.ToArray());
+
+        totalWatch.Stop(); // 停止总计时
+        Console.ForegroundColor = ConsoleColor.Green;
+        Console.WriteLine($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId} 总耗时:{totalWatch.ElapsedMilliseconds:N0} ms");
+    }
+
     /// <summary>
     /// <summary>
     /// 初始化数据库
     /// 初始化数据库
     /// </summary>
     /// </summary>
@@ -352,6 +391,9 @@ public static class SqlSugarSetup
             InitializeTables(dbProvider, entityTypes, config);
             InitializeTables(dbProvider, entityTypes, config);
         }
         }
 
 
+        // 初始化视图
+        if (config.DbSettings.EnableInitView) InitView(dbProvider);
+
         // 初始化种子数据
         // 初始化种子数据
         if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
         if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
     }
     }
@@ -405,6 +447,15 @@ public static class SqlSugarSetup
     /// <param name="config">数据库连接配置</param>
     /// <param name="config">数据库连接配置</param>
     private static void InitializeTables(SqlSugarScopeProvider dbProvider, List<Type> entityTypes, DbConnectionConfig config)
     private static void InitializeTables(SqlSugarScopeProvider dbProvider, List<Type> entityTypes, DbConnectionConfig config)
     {
     {
+        // 删除视图再初始化表结构,防止因为视图导致无法同步表结构
+        var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList();
+        foreach (var viewType in viewTypeList)
+        {
+            var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误");
+            if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName)))
+                dbProvider.DbMaintenance.DropView(entityInfo.DbTableName);
+        }
+
         int count = 0, sum = entityTypes.Count;
         int count = 0, sum = entityTypes.Count;
         var tasks = entityTypes.Select(entityType => Task.Run(() =>
         var tasks = entityTypes.Select(entityType => Task.Run(() =>
         {
         {

+ 240 - 0
Admin.NET/Admin.NET.Core/Update/AutoVersionUpdate.cs

@@ -0,0 +1,240 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+#if NET9_0_OR_GREATER
+
+using Microsoft.AspNetCore.Builder;
+using XiHan.Framework.Utils.Logging;
+using XiHan.Framework.Utils.Reflections;
+
+namespace Admin.NET.Core.Update;
+
+/// <summary>
+/// 自动版本更新中间件拓展
+/// </summary>
+/// <remarks>
+/// 使用方法
+/// 1.在 Admin.NET.Web.Core 的 Startup.cs 中的 Configure 方法中调用 app.UseAutoVersionUpdate()。
+/// 2.在入口项目 Admin.NET.Web.Entry 的根目录下创建一个名为 UpdateScripts 的文件夹,并在其中放置 .sql 后缀的脚本文件
+/// 3.脚本文件命名格式为版本号,例如 1.0.0.sql、1.0.1.sql 等,版本号应符合语义化版本规范。
+/// 4.脚本的属性:复制到输出目录,设置为:始终复制。
+/// 5.设置主节点的 Admin.NET.Application 的 Configuration/App.json 的 WorkerId 为 1。
+/// 6.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version。
+/// ==================================================
+/// 更新新版本时
+/// 1.需在 UpdateScripts 文件夹中添加新的脚本文件,脚本文件名应为新版本号。
+/// 2.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version
+/// </remarks>
+[SuppressSniffer]
+public static class AutoVersionUpdate
+{
+    /// <summary>
+    /// 使用自动版本更新中间件
+    /// </summary>
+    /// <param name="app"></param>
+    /// <returns></returns>
+    public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app)
+    {
+        ConsoleLogger.Info("AutoVersionUpdate 中间件运行");
+
+        var snowIdOpt = App.GetConfig<SnowIdOptions>("SnowId", true);
+        if (snowIdOpt.WorkerId != 1)
+        {
+            ConsoleLogger.Handle("非主节点,不执行脚本");
+            return app;
+        }
+
+        var currentVersion = GetEntryAssemblyCurrentVersion();
+        ConsoleLogger.Handle($"当前版本:{currentVersion}");
+
+        var historyVersionInfo = GetEntryAssemblyHistoryVersionInfo();
+        var historyVersion = historyVersionInfo.Version;
+        var historyDate = historyVersionInfo.Date;
+        var historyIsRunScript = historyVersionInfo.IsRunScript;
+
+        ConsoleLogger.Handle($"历史版本:{historyVersion},更新时间:{historyDate},是否已执行{historyIsRunScript}");
+
+        // 历史版本为空、版本号相同,不执行脚本
+        if (historyVersion == string.Empty)
+        {
+            ConsoleLogger.Handle("历史版本为空,默认为最新版本,不执行脚本");
+
+            // 保存当前版本信息
+            SetEntryAssemblyCurrentVersion(currentVersion, true);
+
+            return app;
+        }
+        else if (currentVersion.CompareTo(historyVersion) <= 0 && historyIsRunScript)
+        {
+            ConsoleLogger.Handle("当前版本号与历史版本号相同,且已执行过脚本,不再执行");
+
+            // 保存当前版本信息
+            SetEntryAssemblyCurrentVersion(currentVersion, false);
+
+            return app;
+        }
+        else
+        {
+            ConsoleLogger.Handle("当前版本号与历史版本号不同,或版本号相同但未执行过脚本,开始执行脚本");
+
+            var scriptSqlVersions = GetScriptSqlVersions();
+
+            // 若不存在当前版本的脚本,则只保存当前版本信息,不执行脚本
+            if (scriptSqlVersions.All(s => s.Version.CompareTo(currentVersion) < 0))
+            {
+                ConsoleLogger.Handle("不存在当前版本的脚本,只保存当前版本信息,不执行脚本");
+
+                // 保存当前版本信息
+                SetEntryAssemblyCurrentVersion(currentVersion, false);
+
+                return app;
+            }
+
+            // 执行脚本
+            foreach (var sqlFileInfo in scriptSqlVersions)
+            {
+                var sqlVersion = sqlFileInfo.Version;
+
+                // 只执行大于历史版本的脚本,或者当前版本但未执行过
+                if (sqlVersion.CompareTo(historyVersion) < 0)
+                {
+                    ConsoleLogger.Handle($"版本{sqlVersion}低于历史版本,跳过");
+                    continue;
+                }
+                if (sqlVersion == historyVersion && historyIsRunScript)
+                {
+                    ConsoleLogger.Handle($"版本{sqlVersion}等于历史版本,且已执行过脚本,跳过");
+                    continue;
+                }
+
+                // 执行脚本
+                var sql = File.ReadAllText(sqlFileInfo.FilePath);
+                if (sql != null)
+                {
+                    ConsoleLogger.Handle($"执行版本{sqlVersion}脚本");
+
+                    HandleSqlScript(app, sql, sqlVersion);
+                }
+            }
+        }
+
+        ConsoleLogger.Success("AutoVersionUpdate 中间件结束");
+
+        return app;
+    }
+
+    #region 辅助方法
+
+    /// <summary>
+    /// 获取入口程序集当前版本信息
+    /// </summary>
+    /// <returns></returns>
+    private static string GetEntryAssemblyCurrentVersion()
+    {
+        var entryAssemblyVersion = AssemblyHelper.GetEntryAssemblyVersion();
+        return entryAssemblyVersion.ToString(3);
+    }
+
+    /// <summary>
+    /// 设置入口程序集当前版本信息
+    /// </summary>
+    /// <param name="version"></param>
+    /// <param name="isRunScript"></param>
+    private static void SetEntryAssemblyCurrentVersion(string version, bool isRunScript)
+    {
+        var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
+        var now = DateTime.Now;
+        File.WriteAllText(path, $"{version}^{now:yyyy-MM-dd HH:mm:ss}^{isRunScript}");
+    }
+
+    /// <summary>
+    /// 获取入口程序集上一次运行版本信息
+    /// </summary>
+    /// <returns></returns>
+    private static HistoryVersionInfo GetEntryAssemblyHistoryVersionInfo()
+    {
+        var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
+
+        // 检查文件是否存在
+        if (File.Exists(path))
+        {
+            // 文件存在时读取内容
+            var info = File.ReadAllText(path);
+
+            if (info.Contains('^'))
+            {
+                var parts = info.Split('^');
+                var version = parts.Length > 0 ? parts[0].ToString() : string.Empty;
+                var date = parts.Length > 1 ? parts[1] : string.Empty;
+                var isRunScript = parts.Length > 2 ? parts[2].ToBoolean() : false;
+
+                return new HistoryVersionInfo(version, date, isRunScript);
+            }
+        }
+
+        // 文件不存在或内容格式不正确时返回默认值
+        return new HistoryVersionInfo(string.Empty, string.Empty, false);
+    }
+
+    /// <summary>
+    /// 获取程序目录下的脚本 SQL 文件版本
+    /// </summary>
+    /// <returns></returns>
+    private static List<SqlFileInfo> GetScriptSqlVersions()
+    {
+        // 获取所有脚本文件
+        var path = Path.Combine(AppContext.BaseDirectory, "UpdateScripts");
+        var scriptFiles = Directory.GetFiles(path, "*.sql").ToList();
+
+        var sqlVersions = scriptFiles
+            .Select(s => new SqlFileInfo(Path.GetFileNameWithoutExtension(s), s))
+            .OrderBy(s => s.Version).ToList();
+        return sqlVersions;
+    }
+
+    /// <summary>
+    /// 保存当前版本信息
+    /// </summary>
+    /// <param name="app"></param>
+    /// <param name="sql"></param>
+    /// <param name="sqlVersion"></param>
+    private static void HandleSqlScript(IApplicationBuilder app, string sql, string sqlVersion)
+    {
+        using var scope = App.GetRequiredService<IServiceScopeFactory>().CreateScope();
+        var dbContext = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
+
+        var isSuccess = false;
+
+        try
+        {
+            // 开启事务
+            dbContext.Ado.BeginTran();
+            dbContext.Ado.ExecuteCommand(sql);
+            dbContext.Ado.CommitTran();
+            isSuccess = true;
+        }
+        catch (Exception ex)
+        {
+            dbContext.Ado.RollbackTran();
+            ConsoleLogger.Error($"AutoVersionUpdate 执行 SQL 脚本出错,版本:{sqlVersion},错误:{ex.Message}");
+        }
+        finally
+        {
+            if (isSuccess)
+            {
+                // 保存当前版本信息
+                SetEntryAssemblyCurrentVersion(sqlVersion, true);
+            }
+        }
+    }
+
+    #endregion 辅助方法
+}
+
+public record SqlFileInfo(string Version, string FilePath);
+public record HistoryVersionInfo(string Version, string Date, bool IsRunScript);
+
+#endif // NET9_0_OR_GREATER

+ 1 - 0
Admin.NET/Admin.NET.Core/Utils/ComputerUtil.cs

@@ -282,6 +282,7 @@ public class MemoryMetrics
     /// CPU使用率%
     /// CPU使用率%
     /// </summary>
     /// </summary>
     public List<string> CpuRates { get; set; }
     public List<string> CpuRates { get; set; }
+
     public string CpuRate { get; set; }
     public string CpuRate { get; set; }
 
 
     /// <summary>
     /// <summary>

+ 4 - 3
Admin.NET/Admin.NET.Core/Utils/FileHelper.cs

@@ -62,6 +62,7 @@ public static class FileHelper
             CopyDirectory(directory, dest, overwrite);
             CopyDirectory(directory, dest, overwrite);
         }
         }
     }
     }
+
     /// <summary>
     /// <summary>
     /// 在文件倒数第lastIndex个identifier前插入内容(备份原文件)
     /// 在文件倒数第lastIndex个identifier前插入内容(备份原文件)
     /// </summary>
     /// </summary>
@@ -73,7 +74,7 @@ public static class FileHelper
     public static async Task InsertsStringAtSpecifiedLocationInFile(string filePath, string insertContent, char identifier, int lastIndex, bool createBackup = false)
     public static async Task InsertsStringAtSpecifiedLocationInFile(string filePath, string insertContent, char identifier, int lastIndex, bool createBackup = false)
     {
     {
         // 参数校验
         // 参数校验
-        if (lastIndex  < 1) throw new ArgumentOutOfRangeException(nameof(lastIndex));
+        if (lastIndex < 1) throw new ArgumentOutOfRangeException(nameof(lastIndex));
         if (identifier == 0) throw new ArgumentException("标识符不能为空字符");
         if (identifier == 0) throw new ArgumentException("标识符不能为空字符");
 
 
         if (!File.Exists(filePath))
         if (!File.Exists(filePath))
@@ -86,8 +87,8 @@ public static class FileHelper
             File.Copy(filePath, backupPath, true);
             File.Copy(filePath, backupPath, true);
         }
         }
 
 
-        using var reader  = new StreamReader(filePath, Encoding.UTF8);
-        var       content = await reader.ReadToEndAsync();
+        using var reader = new StreamReader(filePath, Encoding.UTF8);
+        var content = await reader.ReadToEndAsync();
         reader.Close();
         reader.Close();
         // 逆向查找算法
         // 逆向查找算法
         int index = content.LastIndexOf(identifier);
         int index = content.LastIndexOf(identifier);

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

@@ -12,11 +12,11 @@
     </PropertyGroup>
     </PropertyGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-      <PackageReference Include="Furion.Xunit" Version="4.9.7.59" />
-      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
-      <PackageReference Include="Selenium.Support" Version="4.32.0" />
-      <PackageReference Include="Selenium.WebDriver" Version="4.32.0" />
-      <PackageReference Include="Selenium.WebDriver.MSEdgeDriver" Version="136.0.3240.50" />
+      <PackageReference Include="Furion.Xunit" Version="4.9.7.75" />
+      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
+      <PackageReference Include="Selenium.Support" Version="4.33.0" />
+      <PackageReference Include="Selenium.WebDriver" Version="4.33.0" />
+      <PackageReference Include="Selenium.WebDriver.MSEdgeDriver" Version="136.0.3240.76" />
       <PackageReference Include="xunit.assert" Version="2.9.3" />
       <PackageReference Include="xunit.assert" Version="2.9.3" />
     </ItemGroup>
     </ItemGroup>
 
 

+ 1 - 1
Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj

@@ -11,7 +11,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="IGeekFan.AspNetCore.Knife4jUI" Version="0.0.16" />
     <PackageReference Include="IGeekFan.AspNetCore.Knife4jUI" Version="0.0.16" />
-    <PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.4" />
+    <PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.5" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

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

@@ -1,4 +1,4 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
 //
 //
 // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
 // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
 //
 //
@@ -31,6 +31,10 @@ using System.Text.Json;
 using System.Text.Unicode;
 using System.Text.Unicode;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
+#if NET9_0_OR_GREATER
+using Admin.NET.Core.Update;
+#endif
+
 namespace Admin.NET.Web.Core;
 namespace Admin.NET.Web.Core;
 
 
 [AppStartup(int.MaxValue)]
 [AppStartup(int.MaxValue)]
@@ -94,7 +98,8 @@ public class Startup : AppStartup
             // setting.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; // 解决DateTimeOffset异常
             // setting.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; // 解决DateTimeOffset异常
             // setting.DateParseHandling = DateParseHandling.None; // 解决DateTimeOffset异常
             // setting.DateParseHandling = DateParseHandling.None; // 解决DateTimeOffset异常
             // setting.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); // 解决DateTimeOffset异常
             // setting.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); // 解决DateTimeOffset异常
-        };
+        }
+        ;
 
 
         services.AddControllersWithViews()
         services.AddControllersWithViews()
             .AddAppLocalization()
             .AddAppLocalization()
@@ -350,6 +355,10 @@ public class Startup : AppStartup
             }
             }
         });
         });
 
 
+#if NET9_0_OR_GREATER
+        app.UseAutoVersionUpdate();
+#endif
+
         app.UseEndpoints(endpoints =>
         app.UseEndpoints(endpoints =>
         {
         {
             // 注册集线器
             // 注册集线器

+ 6 - 0
Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,6 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
     <Description>Admin.NET 通用权限开发平台</Description>
+    <AssemblyVersion>1.0.0</AssemblyVersion>
+    <FileVersion>1.0.0</FileVersion>
+    <Version>1.0.0</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -40,6 +43,9 @@
     <None Update="ip2region.db">
     <None Update="ip2region.db">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
     </None>
+    <None Update="UpdateScripts\1.0.0.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 3 - 0
Admin.NET/Admin.NET.Web.Entry/UpdateScripts/1.0.0.sql

@@ -0,0 +1,3 @@
+-- 1.0.0.sql
+-- update
+-- 2025-05-24 11:00:00

+ 6 - 0
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkRoleUser.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkRoleJob.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListOutput.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListResult.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleResult.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistOutput.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistResult.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleListInput.cs

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

+ 7 - 1
Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleSimplelistInput.cs

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

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

@@ -25,8 +25,8 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
     <PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
-    <PackageReference Include="Rezero.Api" Version="1.8.12" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
+    <PackageReference Include="Rezero.Api" Version="1.8.20" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 1 - 0
README.md

@@ -94,6 +94,7 @@ Admin.NET 是基于 .NET6 (Furion/SqlSugar) 实现的通用权限开发框架,
 23. ES 日志:通过 NEST 组件实现日志存取到 Elasticsearch 日志系统。
 23. ES 日志:通过 NEST 组件实现日志存取到 Elasticsearch 日志系统。
 24. 开放授权:支持OAuth 2.0开放标准授权登录,比如微信。
 24. 开放授权:支持OAuth 2.0开放标准授权登录,比如微信。
 25. APIJSON:适配腾讯APIJSON协议,支持后端0代码,[使用文档](https://github.com/liaozb/APIJSON.NET)。
 25. APIJSON:适配腾讯APIJSON协议,支持后端0代码,[使用文档](https://github.com/liaozb/APIJSON.NET)。
+26. 数据库视图:基于SqlSugar生成查询SQL + 表实体维护视图,可维护性更强。
 
 
 ## 🛒应用商城
 ## 🛒应用商城
 
 

+ 39 - 24
Web/package.json

@@ -2,7 +2,7 @@
 	"name": "admin.net",
 	"name": "admin.net",
 	"type": "module",
 	"type": "module",
 	"version": "2.4.33",
 	"version": "2.4.33",
-	"lastBuildTime": "2025.05.04",
+	"lastBuildTime": "2025.05.25",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
 	"author": "zuohuaijun",
 	"author": "zuohuaijun",
 	"license": "MIT",
 	"license": "MIT",
@@ -19,12 +19,12 @@
 	"dependencies": {
 	"dependencies": {
 		"@element-plus/icons-vue": "^2.3.1",
 		"@element-plus/icons-vue": "^2.3.1",
 		"@logicflow/core": "^2.0.13",
 		"@logicflow/core": "^2.0.13",
-		"@logicflow/extension": "^2.0.17",
+		"@logicflow/extension": "^2.0.18",
 		"@microsoft/signalr": "^8.0.7",
 		"@microsoft/signalr": "^8.0.7",
 		"@vue-office/docx": "^1.6.3",
 		"@vue-office/docx": "^1.6.3",
 		"@vue-office/excel": "^1.7.14",
 		"@vue-office/excel": "^1.7.14",
 		"@vue-office/pdf": "^2.0.10",
 		"@vue-office/pdf": "^2.0.10",
-		"@vueuse/core": "^13.1.0",
+		"@vueuse/core": "^13.2.0",
 		"@wangeditor/editor": "^5.1.23",
 		"@wangeditor/editor": "^5.1.23",
 		"@wangeditor/editor-for-vue": "^5.1.12",
 		"@wangeditor/editor-for-vue": "^5.1.12",
 		"animate.css": "^4.1.1",
 		"animate.css": "^4.1.1",
@@ -35,17 +35,17 @@
 		"echarts": "^5.6.0",
 		"echarts": "^5.6.0",
 		"echarts-gl": "^2.0.9",
 		"echarts-gl": "^2.0.9",
 		"echarts-wordcloud": "^2.1.0",
 		"echarts-wordcloud": "^2.1.0",
-		"element-plus": "^2.9.9",
+		"element-plus": "^2.9.11",
 		"ezuikit-js": "^8.1.9-beta.3",
 		"ezuikit-js": "^8.1.9-beta.3",
 		"js-cookie": "^3.0.5",
 		"js-cookie": "^3.0.5",
 		"js-table2excel": "^1.1.2",
 		"js-table2excel": "^1.1.2",
 		"json-editor-vue": "^0.18.1",
 		"json-editor-vue": "^0.18.1",
 		"jsplumb": "^2.15.6",
 		"jsplumb": "^2.15.6",
 		"lodash-es": "^4.17.21",
 		"lodash-es": "^4.17.21",
-		"md-editor-v3": "^5.5.0",
+		"md-editor-v3": "^5.6.0",
 		"mitt": "^3.0.1",
 		"mitt": "^3.0.1",
 		"monaco-editor": "^0.52.2",
 		"monaco-editor": "^0.52.2",
-		"mqtt": "^5.12.0",
+		"mqtt": "^5.13.0",
 		"nprogress": "^0.2.0",
 		"nprogress": "^0.2.0",
 		"pinia": "^3.0.2",
 		"pinia": "^3.0.2",
 		"print-js": "^1.6.0",
 		"print-js": "^1.6.0",
@@ -56,15 +56,15 @@
 		"screenfull": "^6.0.2",
 		"screenfull": "^6.0.2",
 		"sm-crypto-v2": "^1.11.0",
 		"sm-crypto-v2": "^1.11.0",
 		"sortablejs": "^1.15.6",
 		"sortablejs": "^1.15.6",
-		"splitpanes": "^4.0.3",
+		"splitpanes": "^4.0.4",
 		"vcrontab-3": "^3.3.22",
 		"vcrontab-3": "^3.3.22",
 		"vform3-builds": "^3.0.10",
 		"vform3-builds": "^3.0.10",
-		"vue": "^3.5.13",
+		"vue": "^3.5.14",
 		"vue-clipboard3": "^2.0.0",
 		"vue-clipboard3": "^2.0.0",
 		"vue-demi": "^0.14.10",
 		"vue-demi": "^0.14.10",
 		"vue-draggable-plus": "^0.6.0",
 		"vue-draggable-plus": "^0.6.0",
 		"vue-grid-layout": "3.0.0-beta1",
 		"vue-grid-layout": "3.0.0-beta1",
-		"vue-i18n": "^11.1.3",
+		"vue-i18n": "^11.1.4",
 		"vue-json-pretty": "^2.4.0",
 		"vue-json-pretty": "^2.4.0",
 		"vue-plugin-hiprint": "^0.0.60",
 		"vue-plugin-hiprint": "^0.0.60",
 		"vue-router": "^4.5.1",
 		"vue-router": "^4.5.1",
@@ -74,40 +74,55 @@
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
 		"@eslint/eslintrc": "^3.3.1",
 		"@eslint/eslintrc": "^3.3.1",
-		"@eslint/js": "^9.26.0",
+		"@eslint/js": "^9.27.0",
 		"@plugin-web-update-notification/vite": "^2.0.0",
 		"@plugin-web-update-notification/vite": "^2.0.0",
 		"@types/lodash-es": "^4.17.12",
 		"@types/lodash-es": "^4.17.12",
-		"@types/node": "^22.15.3",
+		"@types/node": "^22.15.21",
 		"@types/nprogress": "^0.2.3",
 		"@types/nprogress": "^0.2.3",
 		"@types/sortablejs": "^1.15.8",
 		"@types/sortablejs": "^1.15.8",
-		"@typescript-eslint/eslint-plugin": "^8.32.0",
-		"@typescript-eslint/parser": "^8.32.0",
-		"@vitejs/plugin-vue": "^5.2.3",
-		"@vitejs/plugin-vue-jsx": "^4.1.2",
-		"@vue/compiler-sfc": "^3.5.13",
+		"@typescript-eslint/eslint-plugin": "^8.32.1",
+		"@typescript-eslint/parser": "^8.32.1",
+		"@vitejs/plugin-vue": "^5.2.4",
+		"@vitejs/plugin-vue-jsx": "^4.2.0",
+		"@vue/compiler-sfc": "^3.5.14",
 		"code-inspector-plugin": "^0.20.10",
 		"code-inspector-plugin": "^0.20.10",
-		"eslint": "^9.26.0",
+		"eslint": "^9.27.0",
 		"eslint-plugin-vue": "^10.1.0",
 		"eslint-plugin-vue": "^10.1.0",
-		"globals": "^16.1.0",
+		"globals": "^16.2.0",
 		"less": "^4.3.0",
 		"less": "^4.3.0",
 		"prettier": "^3.5.3",
 		"prettier": "^3.5.3",
-		"rollup-plugin-visualizer": "^5.14.0",
-		"sass": "^1.87.0",
-		"terser": "^5.39.0",
+		"rollup-plugin-visualizer": "^6.0.0",
+		"sass": "^1.89.0",
+		"terser": "^5.39.2",
 		"typescript": "^5.8.3",
 		"typescript": "^5.8.3",
-		"vite": "^6.3.4",
+		"vite": "^6.3.5",
 		"vite-plugin-cdn-import": "^1.0.1",
 		"vite-plugin-cdn-import": "^1.0.1",
-		"vite-plugin-compression2": "^1.3.3",
+		"vite-plugin-compression2": "^1.4.0",
 		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vue-eslint-parser": "^10.1.3"
 		"vue-eslint-parser": "^10.1.3"
 	},
 	},
+	"pnpm": {
+		"onlyBuiltDependencies": [
+			"@vue-office/docx",
+			"@vue-office/excel",
+			"@vue-office/pdf"
+		],
+		"ignoredBuiltDependencies": [
+			"@parcel/watcher",
+			"core-js",
+			"es5-ext",
+			"esbuild",
+			"json-editor-vue",
+			"vue-demi"
+		]
+	},
 	"browserslist": [
 	"browserslist": [
 		"> 1%",
 		"> 1%",
 		"last 2 versions",
 		"last 2 versions",
 		"not dead"
 		"not dead"
 	],
 	],
 	"engines": {
 	"engines": {
-		"node": ">=16.0.0",
+		"node": ">=18.0.0",
 		"npm": ">= 7.0.0"
 		"npm": ">= 7.0.0"
 	},
 	},
 	"keywords": [
 	"keywords": [

+ 29 - 8
Web/src/components/sysDict/sysDict.vue

@@ -1,6 +1,6 @@
 <!-- 组件使用文档: https://gitee.com/zuohuaijun/Admin.NET/pulls/1559  -->
 <!-- 组件使用文档: https://gitee.com/zuohuaijun/Admin.NET/pulls/1559  -->
 <script setup lang="ts">
 <script setup lang="ts">
-import { reactive, watch, PropType } from 'vue';
+import { reactive, watch, PropType, onMounted } from 'vue';
 import { DictItem } from '/@/types/global';
 import { DictItem } from '/@/types/global';
 import { useUserInfo } from '/@/stores/userInfo';
 import { useUserInfo } from '/@/stores/userInfo';
 
 
@@ -43,6 +43,10 @@ const props = defineProps({
 		type: Boolean,
 		type: Boolean,
 		default: false,
 		default: false,
 	},
 	},
+	disabled: {
+		type: Boolean,
+		default: false,
+	},
 });
 });
 
 
 const state = reactive({
 const state = reactive({
@@ -52,7 +56,11 @@ const state = reactive({
 });
 });
 
 
 const setDictValue = (value: any) => {
 const setDictValue = (value: any) => {
-	state.value = value;
+	if (typeof value === 'number' && props.renderAs === 'select') {
+	    state.value = value.toString();
+	} else {
+	    state.value = value;
+	}
 	state.dictData = dictList[props.code]?.filter(props.onItemFilter) ?? [];
 	state.dictData = dictList[props.code]?.filter(props.onItemFilter) ?? [];
 
 
 	if (Array.isArray(value)) {
 	if (Array.isArray(value)) {
@@ -77,18 +85,27 @@ watch(
 	(newValue) => setDictValue(newValue),
 	(newValue) => setDictValue(newValue),
 	{ immediate: true }
 	{ immediate: true }
 );
 );
+onMounted(() => {
+	 if (typeof props.modelValue === 'number' && props.renderAs === 'select') {
+	     state.value = props.modelValue.toString();
+	 } else {
+	     state.value = props.modelValue;
+	 }
+})
 </script>
 </script>
 
 
 <template>
 <template>
 	<!-- 渲染标签 -->
 	<!-- 渲染标签 -->
 	<template v-if="props.renderAs === 'tag'">
 	<template v-if="props.renderAs === 'tag'">
 		<template v-if="Array.isArray(state.dict)">
 		<template v-if="Array.isArray(state.dict)">
-			<el-tag v-for="(item, index) in state.dict" :key="index" v-bind="$attrs" :type="item.tagType" :style="item.styleSetting" :class="item.classSetting" class="mr-1">
+			<el-tag v-for="(item, index) in state.dict" :key="index" v-bind="$attrs" :type="item.tagType"
+				:style="item.styleSetting" :class="item.classSetting" class="mr-1">
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 			</el-tag>
 			</el-tag>
 		</template>
 		</template>
 		<template v-else>
 		<template v-else>
-			<el-tag v-if="state.dict" v-bind="$attrs" :type="state.dict.tagType" :style="state.dict.styleSetting" :class="state.dict.classSetting">
+			<el-tag v-if="state.dict" v-bind="$attrs" :type="state.dict.tagType" :style="state.dict.styleSetting"
+				:class="state.dict.classSetting">
 				{{ onItemFormatter(state.dict) ?? state.dict[props.propLabel] }}
 				{{ onItemFormatter(state.dict) ?? state.dict[props.propLabel] }}
 			</el-tag>
 			</el-tag>
 			<span v-else>{{ state.value }}</span>
 			<span v-else>{{ state.value }}</span>
@@ -96,13 +113,16 @@ watch(
 	</template>
 	</template>
 	<!-- 渲染选择器 -->
 	<!-- 渲染选择器 -->
 	<template v-if="props.renderAs === 'select'">
 	<template v-if="props.renderAs === 'select'">
-		<el-select v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="(newValue: any) => emit('update:modelValue', newValue)">
-			<el-option v-for="(item, index) in state.dictData" :key="index" :label="onItemFormatter(item) ?? item[props.propLabel]" :value="item[props.propValue]" />
+		<el-select :disabled="props.disabled" v-model="state.value" v-bind="$attrs" :multiple="props.multiple"
+			@change="(newValue: any) => emit('update:modelValue', newValue)">
+			<el-option v-for="(item, index) in state.dictData" :key="index"
+				:label="onItemFormatter(item) ?? item[props.propLabel]" :value="item[props.propValue]" />
 		</el-select>
 		</el-select>
 	</template>
 	</template>
 	<!-- 渲染复选框(多选) -->
 	<!-- 渲染复选框(多选) -->
 	<template v-if="props.renderAs === 'checkbox'">
 	<template v-if="props.renderAs === 'checkbox'">
-		<el-checkbox-group v-model="state.value" v-bind="$attrs" @change="(newValue: any) => emit('update:modelValue', newValue)">
+		<el-checkbox-group v-model="state.value" v-bind="$attrs"
+			@change="(newValue: any) => emit('update:modelValue', newValue)">
 			<el-checkbox v-for="(item, index) in state.dictData" :key="index" :label="item[props.propValue]">
 			<el-checkbox v-for="(item, index) in state.dictData" :key="index" :label="item[props.propValue]">
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 			</el-checkbox>
 			</el-checkbox>
@@ -110,7 +130,8 @@ watch(
 	</template>
 	</template>
 	<!-- 渲染单选框 -->
 	<!-- 渲染单选框 -->
 	<template v-if="props.renderAs === 'radio'">
 	<template v-if="props.renderAs === 'radio'">
-		<el-radio-group v-model="state.value" v-bind="$attrs" @change="(newValue: any) => emit('update:modelValue', newValue)">
+		<el-radio-group v-model="state.value" v-bind="$attrs"
+			@change="(newValue: any) => emit('update:modelValue', newValue)">
 			<el-radio v-for="(item, index) in state.dictData" :key="index" :value="item[props.propValue]">
 			<el-radio v-for="(item, index) in state.dictData" :key="index" :value="item[props.propValue]">
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 				{{ onItemFormatter(item) ?? item[props.propLabel] }}
 			</el-radio>
 			</el-radio>

+ 1 - 1
Web/src/views/login/index.vue

@@ -332,7 +332,7 @@ const getTenantInfo = async () => {
 
 
 		/* 保持原有内容可见,放置在伪元素下方 */
 		/* 保持原有内容可见,放置在伪元素下方 */
 		.login-right-warp > * {
 		.login-right-warp > * {
-			position: relative;
+			position: absolute;
 			z-index: 2;
 			z-index: 2;
 		}
 		}
 	}
 	}

+ 52 - 38
Web/src/views/system/tenant/component/editTenant.vue

@@ -3,7 +3,8 @@
 		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="700px">
 		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="700px">
 			<template #header>
 			<template #header>
 				<div style="color: #fff">
 				<div style="color: #fff">
-					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit />
+					</el-icon>
 					<span> {{ props.title }} </span>
 					<span> {{ props.title }} </span>
 				</div>
 				</div>
 			</template>
 			</template>
@@ -12,28 +13,34 @@
 					<el-tab-pane label="基本信息" style="height: 400px; overflow-y: auto; overflow-x: hidden">
 					<el-tab-pane label="基本信息" style="height: 400px; overflow-y: auto; overflow-x: hidden">
 						<el-row :gutter="35">
 						<el-row :gutter="35">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="租户类型" :rules="[{ required: true, message: '租户类型不能为空', trigger: 'blur' }]">
-									<g-sys-dict v-model="state.ruleForm.tenantType" code="TenantTypeEnum" render-as="radio" :disabled="state.ruleForm.id != undefined" />
+								<el-form-item label="租户类型"
+									:rules="[{ required: true, message: '租户类型不能为空', trigger: 'blur' }]">
+									<g-sys-dict v-model="state.ruleForm.tenantType" code="TenantTypeEnum" render-as="radio"
+										:disabled="state.ruleForm.id != undefined" />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="租户名称" prop="name" :rules="[{ required: true, message: '租户名称不能为空', trigger: 'blur' }]">
+								<el-form-item label="租户名称" prop="name"
+									:rules="[{ required: true, message: '租户名称不能为空', trigger: 'blur' }]">
 									<el-input v-model="state.ruleForm.name" placeholder="租户名称" clearable />
 									<el-input v-model="state.ruleForm.name" placeholder="租户名称" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="租管账号" prop="adminAccount" :rules="[{ required: true, message: '租管账号不能为空', trigger: 'blur' }]">
+								<el-form-item label="租管账号" prop="adminAccount"
+									:rules="[{ required: true, message: '租管账号不能为空', trigger: 'blur' }]">
 									<el-input v-model="state.ruleForm.adminAccount" placeholder="租管账号" clearable />
 									<el-input v-model="state.ruleForm.adminAccount" placeholder="租管账号" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="电话" prop="phone" :rules="[{ required: true, message: '电话号码不能为空', trigger: 'blur' }]">
+								<el-form-item label="电话" prop="phone"
+									:rules="[{ required: true, message: '电话号码不能为空', trigger: 'blur' }]">
 									<el-input v-model="state.ruleForm.phone" placeholder="电话" clearable />
 									<el-input v-model="state.ruleForm.phone" placeholder="电话" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 								<el-form-item label="数据库类型">
 								<el-form-item label="数据库类型">
-									<el-select v-model="state.ruleForm.dbType" placeholder="数据库类型" clearable class="w100" :disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined">
+									<el-select v-model="state.ruleForm.dbType" placeholder="数据库类型" clearable class="w100"
+										:disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined">
 										<el-option label="MySql" :value="0" />
 										<el-option label="MySql" :value="0" />
 										<el-option label="SqlServer" :value="1" />
 										<el-option label="SqlServer" :value="1" />
 										<el-option label="Sqlite" :value="2" />
 										<el-option label="Sqlite" :value="2" />
@@ -69,24 +76,17 @@
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 								<el-form-item label="连接字符串">
 								<el-form-item label="连接字符串">
-									<el-input
-										v-model="state.ruleForm.connection"
-										placeholder="连接字符串"
-										clearable
+									<el-input v-model="state.ruleForm.connection" placeholder="连接字符串" clearable
 										type="textarea"
 										type="textarea"
-										:disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined"
-									/>
+										:disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined" />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 								<el-form-item label="从库连接串">
 								<el-form-item label="从库连接串">
-									<el-input
-										v-model="state.ruleForm.slaveConnections"
+									<el-input v-model="state.ruleForm.slaveConnections"
 										placeholder="格式:[{'HitRate':10, 'ConnectionString':'xxx'},{'HitRate':10, 'ConnectionString':'xxx'}]"
 										placeholder="格式:[{'HitRate':10, 'ConnectionString':'xxx'},{'HitRate':10, 'ConnectionString':'xxx'}]"
-										clearable
-										type="textarea"
-										:disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined"
-									/>
+										clearable type="textarea"
+										:disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined" />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -101,54 +101,70 @@
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 								<el-form-item label="备注">
 								<el-form-item label="备注">
-									<el-input v-model="state.ruleForm.remark" placeholder="请输入备注内容" clearable type="textarea" />
+									<el-input v-model="state.ruleForm.remark" placeholder="请输入备注内容" clearable
+										type="textarea" />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 						</el-row>
 						</el-row>
 					</el-tab-pane>
 					</el-tab-pane>
-					<el-tab-pane label="站点信息" style="height: 400px; overflow: auto; overflow-x: hidden">
+					<el-tab-pane label="站点信息" style="height: 400px; overflow: auto; overflow-x: hidden"
+						v-if="state.ruleForm.host?.trim()">
 						<el-row :gutter="35">
 						<el-row :gutter="35">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-								<el-form-item label="Logo" prop="logo" :rules="[{ required: true, message: '应用Logo不能为空', trigger: 'blur' }]">
-									<el-upload ref="uploadRef" class="avatar-uploader" :showFileList="false" :autoUpload="false" accept=".jpg,.png,.svg" action :limit="1" :onChange="handleUploadChange">
-										<img v-if="state.ruleForm.logo" :src="state.ruleForm.logo" class="avatar" style="max-width: 100px; max-height: 100px; object-fit: contain" />
+								<el-form-item label="Logo" prop="logo"
+									:rules="[{ required: true, message: '应用Logo不能为空', trigger: 'blur' }]">
+									<el-upload ref="uploadRef" class="avatar-uploader" :showFileList="false"
+										:autoUpload="false" accept=".jpg,.png,.svg" action :limit="1"
+										:onChange="handleUploadChange">
+										<img v-if="state.ruleForm.logo" :src="state.ruleForm.logo" class="avatar"
+											style="max-width: 100px; max-height: 100px; object-fit: contain" />
 										<SvgIcon v-else class="avatar-uploader-icon" name="ele-Plus" :size="28" />
 										<SvgIcon v-else class="avatar-uploader-icon" name="ele-Plus" :size="28" />
 									</el-upload>
 									</el-upload>
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="标题" prop="title" :rules="[{ required: true, message: '标题不能为空', trigger: 'blur' }]">
+								<el-form-item label="标题" prop="title"
+									:rules="[{ required: true, message: '标题不能为空', trigger: 'blur' }]">
 									<el-input v-model="state.ruleForm.title" placeholder="应用标题" maxlength="32" clearable />
 									<el-input v-model="state.ruleForm.title" placeholder="应用标题" maxlength="32" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="副标题" prop="viceTitle" :rules="[{ required: true, message: '副标题不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.viceTitle" placeholder="应用副标题" maxlength="32" clearable />
+								<el-form-item label="副标题" prop="viceTitle"
+									:rules="[{ required: true, message: '副标题不能为空', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.viceTitle" placeholder="应用副标题" maxlength="32"
+										clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-								<el-form-item label="副标题描述" prop="viceDesc" :rules="[{ required: true, message: '副标题描述不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.viceDesc" placeholder="应用副标题描述" maxlength="64" clearable />
+								<el-form-item label="副标题描述" prop="viceDesc"
+									:rules="[{ required: true, message: '副标题描述不能为空', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.viceDesc" placeholder="应用副标题描述" maxlength="64"
+										clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24" class="mb20">
 							<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24" class="mb20">
-								<el-form-item label="版权信息" prop="copyright" :rules="[{ required: true, message: '版权信息不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.copyright" placeholder="版权信息" maxlength="64" clearable />
+								<el-form-item label="版权信息" prop="copyright"
+									:rules="[{ required: true, message: '版权信息不能为空', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.copyright" placeholder="版权信息" maxlength="64"
+										clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="备案号" prop="icp" :rules="[{ required: true, message: '备案号不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.icp" placeholder="备案号" maxlength="32" clearable />
+								<el-form-item label="备案号" prop="icp">
+									<el-input v-model="state.ruleForm.icp" placeholder="例:省ICP备12345678号" maxlength="32" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="ICP地址" prop="icpUrl" :rules="[{ required: true, message: 'ICP地址不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.icpUrl" placeholder="ICP地址" maxlength="32" clearable />
+								<el-form-item label="ICP地址" prop="icpUrl"
+									:rules="[{ required: state.ruleForm.icp, message: 'ICP地址不能为空', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.icpUrl" placeholder="例:https://beian.miit.gov.cn" maxlength="32"
+										clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 								<el-form-item label="水印" prop="watermark">
 								<el-form-item label="水印" prop="watermark">
-									<el-input v-model="state.ruleForm.watermark" placeholder="如果此处留空,则水印功能将被禁用" maxlength="32" clearable />
+									<el-input v-model="state.ruleForm.watermark" placeholder="如果此处留空,则水印功能将被禁用"
+										maxlength="32" clearable />
 								</el-form-item>
 								</el-form-item>
 							</el-col>
 							</el-col>
 						</el-row>
 						</el-row>
@@ -201,8 +217,6 @@ const openDialog = async (row: any) => {
 	state.selectedTabName = '0';
 	state.selectedTabName = '0';
 	ruleFormRef.value?.resetFields();
 	ruleFormRef.value?.resetFields();
 	state.ruleForm = JSON.parse(JSON.stringify(row));
 	state.ruleForm = JSON.parse(JSON.stringify(row));
-	state.ruleForm.icp ??= '省ICP备12345678号';
-	state.ruleForm.icpUrl ??= 'https://beian.miit.gov.cn';
 	state.ruleForm.copyright ??= `Copyright \u00a9 ${new Date().getFullYear()}-present xxxxx All rights reserved.`;
 	state.ruleForm.copyright ??= `Copyright \u00a9 ${new Date().getFullYear()}-present xxxxx All rights reserved.`;
 	state.isShowDialog = true;
 	state.isShowDialog = true;
 	state.regWayData = await getAPI(SysUserRegWayApi).apiSysUserRegWayListPost({ tenantId: row.id }).then((res) => res.data.result ?? []);
 	state.regWayData = await getAPI(SysUserRegWayApi).apiSysUserRegWayListPost({ tenantId: row.id }).then((res) => res.data.result ?? []);

+ 1 - 1
Web/src/views/system/tenant/index.vue

@@ -228,7 +228,7 @@ const resetQuery = () => {
 // 打开新增页面
 // 打开新增页面
 const openAddTenant = () => {
 const openAddTenant = () => {
 	state.editTenantTitle = '添加租户';
 	state.editTenantTitle = '添加租户';
-	editTenantRef.value?.openDialog({ tenantType: 0, orderNo: 100 });
+	editTenantRef.value?.openDialog({ tenantType: 0, orderNo: 100, host: '' });
 };
 };
 
 
 // 打开编辑页面
 // 打开编辑页面