Procházet zdrojové kódy

feat: 😀完善系统更新功能

喵你个旺呀 před 1 rokem
rodič
revize
7e6b5e40dc

+ 6 - 6
Admin.NET/Admin.NET.Application/Configuration/CDConfig.json

@@ -2,18 +2,18 @@
   "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
 
   "CDConfig": {
-    "Owner": "zuohuaijun", // gitee用户名
+    "Owner": "jasondom", // gitee用户名
     "Repo": "Admin.NET", // 仓库名
     "Branch": "next", // 分支名
     "AccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxx", // gitee用户授权码
-    "UpdateInterval": 10, // 最小更新间隔(分钟),-1不限制
-    "BackupCount": 10, // 备份文件保留数量
+    "UpdateInterval": 0, // 最小更新间隔(分钟),0不限制
+    "BackupCount": 10, // 备份文件保留数量,0不限制
     "BackendOutput": "D:\\Admin.NET", // 后端输出目录
     "Publish": { // 后端发布选项
-      "Configuration": "Release", // 发布环境
+      "Configuration": "Release", // 发布运行版本
       "TargetFramework": "net8.0", // 发布.NET版本
-      "RuntimeIdentifier": "linux-x64" // 运行环境
+      "RuntimeIdentifier": "linux-x64" // 运行平台
     },
-    "ExcludeFiles": ["Configuration\\Database.json"] // 排除文件
+    "ExcludeFiles": ["Configuration\\*.json"] // 排除文件
   }
 }

+ 6 - 1
Admin.NET/Admin.NET.Core/Const/CacheConst.cs

@@ -104,5 +104,10 @@ public class CacheConst
     /// <summary>
     /// 系统更新命令日志缓存
     /// </summary>
-    public const string KeySysUpdateLog = "sys_update_log:";
+    public const string KeySysUpdateLog = "sys_update_log";
+
+    /// <summary>
+    /// 系统更新间隔标记缓存
+    /// </summary>
+    public const string KeySysUpdateInterval = "sys_update_interval";
 }

+ 2 - 4
Admin.NET/Admin.NET.Core/Option/CDConfigOptions.cs

@@ -30,16 +30,14 @@ public class CDConfigOptions : IConfigurableOptions
     /// 用户授权码
     /// </summary>
     public string AccessToken { get; set; }
-    
-
 
     /// <summary>
-    /// 更新间隔限制(分钟)-1 不限制
+    /// 更新间隔限制(分钟)0 不限制
     /// </summary>
     public int UpdateInterval { get; set; }
     
     /// <summary>
-    /// 保留备份文件的数量
+    /// 保留备份文件的数量, 0 不限制
     /// </summary>
     public int BackupCount { get; set; }
 

+ 8 - 0
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -184,6 +184,14 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1300300150501, Pid=1300300150101, Title="获取支付订单详情(微信接口)", Permission="sysWechatPay:payInfoFromWechat", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300300150601, Pid=1300300150101, Title="退款申请", Permission="sysWechatPay:refundDomestic", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
+            new SysMenu{ Id=1300300160101, Pid=1300300000101, Title="系统更新", Path="/platform/update", Name="sysUpdate", Component="/system/update/index", Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=240 },
+            new SysMenu{ Id=1300300160201, Pid=1300300160101, Title="更新", Permission="sysUpdate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300300160301, Pid=1300300160101, Title="还原", Permission="sysUpdate:restore", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300300160401, Pid=1300300160101, Title="备份列表", Permission="sysUpdate:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300300160501, Pid=1300300160101, Title="日志列表", Permission="sysUpdate:logs", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300300160601, Pid=1300300160101, Title="清除日志", Permission="sysUpdate:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300300160701, Pid=1300300160101, Title="获取密钥", Permission="sysUpdate:webHookKey", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+
             new SysMenu{ Id=1300400000101, Pid=0, Title="日志管理", Path="/log", Name="log", Component="Layout", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=12000 },
             new SysMenu{ Id=1300400010101, Pid=1300400000101, Title="访问日志", Path="/log/vislog", Name="sysVisLog", Component="/system/log/vislog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300400010201, Pid=1300400010101, Title="查询", Permission="sysVislog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

+ 31 - 0
Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs

@@ -0,0 +1,31 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 还原输入参数
+/// </summary>
+public class RestoreInput
+{
+    /// <summary>
+    /// 文件名
+    /// </summary>
+    [Required(ErrorMessage = "文件名不能为空")]
+    public string FileName { get; set; }
+}
+
+/// <summary>
+/// WebHook输入参数
+/// </summary>
+public class WebHookInput
+{
+    /// <summary>
+    /// 密钥
+    /// </summary>
+    [Required(ErrorMessage = "密钥不能为空")]
+    public string Key { get; set; }
+}

+ 27 - 0
Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs

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

+ 209 - 28
Admin.NET/Admin.NET.Core/Service/Update/SysUpdateService.cs

@@ -5,6 +5,7 @@
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
 using System.IO.Compression;
+using DocumentFormat.OpenXml.Office2010.ExcelAc;
 
 namespace Admin.NET.Core.Service;
 
@@ -35,9 +36,67 @@ public class SysUpdateService : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 从远端更新项目
+    /// 备份列表
     /// </summary>
     /// <returns></returns>
+    [DisplayName("备份列表")]
+    [ApiDescriptionSettings(Name = "List"), HttpPost]
+    public Task<List<BackupOutput>> List()
+    {
+        const string backendDir = "Admin.NET";
+        var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, ".."));
+        return Task.FromResult(Directory.GetFiles(rootPath, backendDir + "*.zip", SearchOption.TopDirectoryOnly)
+            .Select(filePath =>
+            {
+                var file = new FileInfo(filePath);
+                return new BackupOutput
+                {
+                    CreateTime = file.CreationTime,
+                    FilePath = filePath,
+                    FileName = file.Name
+                };
+            })
+            .OrderByDescending(u => u.CreateTime)
+            .ToList());
+    }
+
+    /// <summary>
+    /// 还原
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("还原")]
+    [ApiDescriptionSettings(Name = "Restore"), HttpPost]
+    public async Task Restore(RestoreInput input)
+    {
+        // 检查参数
+        CheckConfig();
+        try
+        {
+            var file = (await List()).FirstOrDefault(u => u.FileName.EqualIgnoreCase(input.FileName));
+            if (file == null)
+            {
+                await PrintfLog("文件不存在...");
+                return;
+            }
+
+            await PrintfLog("正在还原...");
+            using ZipArchive archive = new(File.OpenRead(file.FilePath), ZipArchiveMode.Read, leaveOpen: false);
+            archive.ExtractToDirectory(_cdConfigOptions.BackendOutput, true);
+            await PrintfLog("还原成功...");
+        }
+        catch (Exception ex)
+        {
+            await PrintfLog("发生异常:" + ex.Message);
+            throw;
+        }
+    }
+
+    /// <summary>
+    /// 从远端更新系统
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("系统更新")]
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
     public async Task Update()
     {
         var originColor = Console.ForegroundColor;
@@ -45,64 +104,95 @@ public class SysUpdateService : IDynamicApiController, ITransient
         Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目");
         try
         {
-            await SendMessage("----------------------------从远端仓库部署项目-开始----------------------------");
-            await SendMessage($"客户端host:{App.HttpContext.Request.Host}");
-            await SendMessage($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}");
-            await SendMessage($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git");
-            await SendMessage($"仓库分支:{_cdConfigOptions.Branch}");
+            await PrintfLog("----------------------------从远端仓库部署项目-开始----------------------------");
+
+            // 检查参数
+            await PrintfLog("检查参数...");
+            CheckConfig();
+
+            // 检查操作间隔
+            if (_cdConfigOptions.UpdateInterval > 0)
+            {
+                if (_sysCacheService.Get<bool>(CacheConst.KeySysUpdateInterval)) throw Oops.Oh("请勿频繁操作");
+                _sysCacheService.Set(CacheConst.KeySysUpdateInterval, true, TimeSpan.FromMinutes(_cdConfigOptions.UpdateInterval));
+            }
 
-            await SendMessage("项目备份...");
-            // TODO 备份项目
+            await PrintfLog($"客户端host:{App.HttpContext.Request.Host}");
+            await PrintfLog($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}");
+            await PrintfLog($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git");
+            await PrintfLog($"仓库分支:{_cdConfigOptions.Branch}");
 
             // 获取解压后的根目录
             var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, ".."));
             var tempDir = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}-{_cdConfigOptions.Branch}");
 
-            await SendMessage("清理旧文件...");
+            await PrintfLog("清理旧文件...");
             FileHelper.TryDelete(tempDir);
 
-            await SendMessage("拉取远端代码...");
+            await PrintfLog("拉取远端代码...");
             var stream = await GiteeHelper.DownloadRepoZip(_cdConfigOptions.Owner, _cdConfigOptions.Repo,
                 _cdConfigOptions.AccessToken, _cdConfigOptions.Branch);
 
-            await SendMessage("文件包解压...");
+            await PrintfLog("文件包解压...");
             using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false);
             archive.ExtractToDirectory(rootPath);
 
             // 项目目录
-            var backendDir = "Admin.NET";
-            var entryProjectName = "Admin.NET.Web.Entry";
-            var tempOutput = Path.Combine(_cdConfigOptions.BackendOutput, "temp");
-
-            await SendMessage("编译项目...");
-            await SendMessage($"发布版本:{_cdConfigOptions.Publish.Configuration}");
-            await SendMessage($"目标框架:{_cdConfigOptions.Publish.TargetFramework}");
-            await SendMessage($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}");
+            var backendDir = "Admin.NET"; // 后端根目录
+            var entryProjectName = "Admin.NET.Web.Entry"; // 启动项目目录
+            var tempOutput = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_temp");
+
+            await PrintfLog("编译项目...");
+            await PrintfLog($"发布版本:{_cdConfigOptions.Publish.Configuration}");
+            await PrintfLog($"目标框架:{_cdConfigOptions.Publish.TargetFramework}");
+            await PrintfLog($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}");
             var option = _cdConfigOptions.Publish;
             var adminNetDir = Path.Combine(tempDir, backendDir);
             var args = $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{tempOutput}\"";
             await RunCommandAsync("dotnet", args, adminNetDir);
 
-            await SendMessage("移动wwwroot目录...");
+            await PrintfLog("复制 wwwroot 目录...");
             var wwwrootDir = Path.Combine(adminNetDir, entryProjectName, "wwwroot");
             FileHelper.CopyDirectory(wwwrootDir, Path.Combine(tempOutput, "wwwroot"), true);
 
             // 删除排除文件
-            await SendMessage("删除排除文件...");
-            foreach (var file in _cdConfigOptions.ExcludeFiles ?? new()) FileHelper.TryDelete(Path.Combine(tempOutput, file));
+            foreach (var filePath in (_cdConfigOptions.ExcludeFiles ?? new()).SelectMany(file => Directory.GetFiles(tempOutput, file, SearchOption.TopDirectoryOnly)))
+            {
+                await PrintfLog($"排除文件:{filePath}");
+                FileHelper.TryDelete(filePath);
+            }
+
+            await PrintfLog("备份原项目文件...");
+            string backupPath = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_{DateTime.Now:yyyy_MM_dd}.zip") ;
+            if (File.Exists(backupPath)) File.Delete(backupPath);
+            ZipFile.CreateFromDirectory(_cdConfigOptions.BackendOutput, backupPath);
 
             // 将临时文件移动到正式目录
             FileHelper.CopyDirectory(tempOutput, _cdConfigOptions.BackendOutput, true);
 
-            await SendMessage("清理文件...");
+            await PrintfLog("清理文件...");
             FileHelper.TryDelete(tempOutput);
             FileHelper.TryDelete(tempDir);
 
-            await SendMessage("----------------------------从远端仓库部署项目-结束----------------------------");
+            if (_cdConfigOptions.BackupCount > 0)
+            {
+                var fileList = await List();
+                if (fileList.Count > _cdConfigOptions.BackupCount)
+                    await PrintfLog("清除多余的备份文件...");
+                while (fileList.Count > _cdConfigOptions.BackupCount)
+                {
+                    var last = fileList.Last();
+                    FileHelper.TryDelete(last.FilePath);
+                    fileList.Remove(last);
+                }
+            }
+
+            await PrintfLog("重启项目后生效...");
+            await PrintfLog("----------------------------从远端仓库部署项目-结束----------------------------");
         }
         catch (Exception ex)
         {
-            await SendMessage("发生异常:" + ex.Message);
+            await PrintfLog("发生异常:" + ex.Message);
             throw;
         }
         finally
@@ -112,10 +202,101 @@ public class SysUpdateService : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 推送消息
+    /// 仓库WebHook接口
+    /// </summary>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("仓库WebHook接口")]
+    [ApiDescriptionSettings(Name = "WebHook"), HttpPost]
+    public async Task WebHook(WebHookInput input)
+    {
+        if (CryptogramUtil.Decrypt(input.Key) != GetWebHookKeyPlainText()) throw Oops.Oh("非法密钥");
+        var updateInterval = _cdConfigOptions.UpdateInterval;
+        try
+        {
+            _cdConfigOptions.UpdateInterval = 0;
+            await Update();
+        }
+        finally
+        {
+            _cdConfigOptions.UpdateInterval = updateInterval;
+        }
+    }
+
+    /// <summary>
+    /// 获取WebHook接口密钥
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取WebHook接口密钥")]
+    [ApiDescriptionSettings(Name = "WebHookKey"), HttpGet]
+    public string GetWebHookKey()
+    {
+        return CryptogramUtil.Encrypt(GetWebHookKeyPlainText());
+    }
+
+    /// <summary>
+    /// 获取日志列表
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取日志列表")]
+    [ApiDescriptionSettings(Name = "Logs"), HttpGet]
+    public List<string> LogList()
+    {
+        return _sysCacheService.Get<List<string>>(CacheConst.KeySysUpdateLog) ?? new();
+    }
+
+    /// <summary>
+    /// 清空日志
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("清空日志")]
+    [ApiDescriptionSettings(Name = "Clear"), HttpGet]
+    public void ClearLog()
+    {
+        _sysCacheService.Remove(CacheConst.KeySysUpdateLog);
+    }
+
+    /// <summary>
+    /// 获取密钥明文
+    /// </summary>
+    /// <returns></returns>
+    private string GetWebHookKeyPlainText()
+    {
+        return $"https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git-{_cdConfigOptions.Branch}-{_cdConfigOptions.AccessToken}";
+    }
+
+    /// <summary>
+    /// 检查参数
+    /// </summary>
+    /// <returns></returns>
+    private void CheckConfig()
+    {
+        if (_cdConfigOptions == null) throw Oops.Oh("CDConfig配置不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.Owner)) throw Oops.Oh("仓库用户名不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.Repo)) throw Oops.Oh("仓库名不能为空");
+
+        // if (string.IsNullOrWhiteSpace(_cdConfigOptions.Branch)) throw Oops.Oh("分支名不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.AccessToken)) throw Oops.Oh("授权信息不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.BackendOutput)) throw Oops.Oh("部署目录不能为空");
+
+        if (_cdConfigOptions.Publish == null) throw Oops.Oh("编译配置不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.Configuration)) throw Oops.Oh("运行环境编译配置不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.TargetFramework)) throw Oops.Oh(".NET版本编译配置不能为空");
+
+        if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.RuntimeIdentifier)) throw Oops.Oh("运行平台配置不能为空");
+    }
+
+    /// <summary>
+    /// 打印日志
     /// </summary>
     /// <param name="message"></param>
-    public Task SendMessage(string message)
+    private Task PrintfLog(string message)
     {
         var logList = _sysCacheService.Get<List<string>>(CacheConst.KeySysUpdateLog) ?? new();
 
@@ -159,7 +340,7 @@ public class SysUpdateService : IDynamicApiController, ITransient
         {
             string line = await process.StandardOutput.ReadLineAsync();
             if (string.IsNullOrEmpty(line)) continue;
-            await SendMessage(line.Trim());
+            await PrintfLog(line.Trim());
         }
         await process.WaitForExitAsync();
     }

+ 1 - 0
Web/src/api-services/api.ts

@@ -47,6 +47,7 @@ export * from './apis/sys-schedule-api';
 export * from './apis/sys-server-api';
 export * from './apis/sys-sms-api';
 export * from './apis/sys-tenant-api';
+export * from './apis/sys-update-api';
 export * from './apis/sys-user-api';
 export * from './apis/sys-user-menu-api';
 export * from './apis/sys-user-reg-way-api';

+ 4 - 3
Web/src/api-services/apis/sys-menu-api.ts

@@ -17,6 +17,7 @@ import { Configuration } from '../configuration';
 // @ts-ignore
 import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
 import { AddMenuInput } from '../models';
+import { AdminResultInt64 } from '../models';
 import { AdminResultListMenuOutput } from '../models';
 import { AdminResultListString } from '../models';
 import { AdminResultListSysMenu } from '../models';
@@ -333,7 +334,7 @@ export const SysMenuApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+        async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultInt64>>> {
             const localVarAxiosArgs = await SysMenuApiAxiosParamCreator(configuration).apiSysMenuAddPost(body, options);
             return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
                 const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
@@ -426,7 +427,7 @@ export const SysMenuApiFactory = function (configuration?: Configuration, basePa
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+        async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultInt64>> {
             return SysMenuApiFp(configuration).apiSysMenuAddPost(body, options).then((request) => request(axios, basePath));
         },
         /**
@@ -497,7 +498,7 @@ export class SysMenuApi extends BaseAPI {
      * @throws {RequiredError}
      * @memberof SysMenuApi
      */
-    public async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+    public async apiSysMenuAddPost(body?: AddMenuInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultInt64>> {
         return SysMenuApiFp(this.configuration).apiSysMenuAddPost(body, options).then((request) => request(this.axios, this.basePath));
     }
     /**

+ 599 - 0
Web/src/api-services/apis/sys-update-api.ts

@@ -0,0 +1,599 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import globalAxios, { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios';
+import { Configuration } from '../configuration';
+// Some imports not used depending on template conditions
+// @ts-ignore
+import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
+import { AdminResultListBackupOutput } from '../models';
+import { AdminResultListString } from '../models';
+import { AdminResultString } from '../models';
+import { RestoreInput } from '../models';
+import { WebHookInput } from '../models';
+/**
+ * SysUpdateApi - axios parameter creator
+ * @export
+ */
+export const SysUpdateApiAxiosParamCreator = function (configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 清空日志
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateClearGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/clear`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 备份列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateListPost: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/list`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 获取日志列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateLogsGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/logs`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 还原
+         * @param {RestoreInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateRestorePost: async (body?: RestoreInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/restore`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 从远端更新系统
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateUpdatePost: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/update`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 获取WebHook接口密钥
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateWebHookKeyGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/webHookKey`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 仓库WebHook接口
+         * @param {WebHookInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUpdateWebHookPost: async (body?: WebHookInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUpdate/webHook`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+    }
+};
+
+/**
+ * SysUpdateApi - functional programming interface
+ * @export
+ */
+export const SysUpdateApiFp = function(configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 清空日志
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateClearGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateClearGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 备份列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateListPost(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListBackupOutput>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateListPost(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 获取日志列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateLogsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListString>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateLogsGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 还原
+         * @param {RestoreInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateRestorePost(body?: RestoreInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateRestorePost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 从远端更新系统
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateUpdatePost(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateUpdatePost(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 获取WebHook接口密钥
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateWebHookKeyGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultString>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateWebHookKeyGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 仓库WebHook接口
+         * @param {WebHookInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateWebHookPost(body?: WebHookInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUpdateApiAxiosParamCreator(configuration).apiSysUpdateWebHookPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+    }
+};
+
+/**
+ * SysUpdateApi - factory interface
+ * @export
+ */
+export const SysUpdateApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    return {
+        /**
+         * 
+         * @summary 清空日志
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateClearGet(options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateClearGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 备份列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateListPost(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListBackupOutput>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateListPost(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取日志列表
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateLogsGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListString>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateLogsGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 还原
+         * @param {RestoreInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateRestorePost(body?: RestoreInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateRestorePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 从远端更新系统
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateUpdatePost(options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateUpdatePost(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取WebHook接口密钥
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateWebHookKeyGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultString>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateWebHookKeyGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 仓库WebHook接口
+         * @param {WebHookInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUpdateWebHookPost(body?: WebHookInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUpdateApiFp(configuration).apiSysUpdateWebHookPost(body, options).then((request) => request(axios, basePath));
+        },
+    };
+};
+
+/**
+ * SysUpdateApi - object-oriented interface
+ * @export
+ * @class SysUpdateApi
+ * @extends {BaseAPI}
+ */
+export class SysUpdateApi extends BaseAPI {
+    /**
+     * 
+     * @summary 清空日志
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateClearGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateClearGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 备份列表
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateListPost(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListBackupOutput>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateListPost(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取日志列表
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateLogsGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListString>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateLogsGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 还原
+     * @param {RestoreInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateRestorePost(body?: RestoreInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateRestorePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 从远端更新系统
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateUpdatePost(options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateUpdatePost(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取WebHook接口密钥
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateWebHookKeyGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultString>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateWebHookKeyGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 仓库WebHook接口
+     * @param {WebHookInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUpdateApi
+     */
+    public async apiSysUpdateWebHookPost(body?: WebHookInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUpdateApiFp(this.configuration).apiSysUpdateWebHookPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+}

+ 11 - 23
Web/src/api-services/models/add-tenant-input.ts

@@ -87,6 +87,12 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     orgId?: number;
+    /**
+     * 域名
+     * @type {string}
+     * @memberof AddTenantInput
+     */
+    host?: string | null;
     /**
      * 
      * @type {TenantTypeEnum}
@@ -130,23 +136,17 @@ export interface AddTenantInput {
      */
     regWayId?: number | null;
     /**
-     * 
-     * @type {YesNoEnum}
-     * @memberof AddTenantInput
-     */
-    captcha?: YesNoEnum;
-    /**
-     * 
-     * @type {YesNoEnum}
+     * 图标
+     * @type {string}
      * @memberof AddTenantInput
      */
-    secondVer?: YesNoEnum;
+    logo: string;
     /**
-     * 图标
+     * 水印
      * @type {string}
      * @memberof AddTenantInput
      */
-    logo: string;
+    watermark?: string | null;
     /**
      * 排序
      * @type {number}
@@ -189,12 +189,6 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     adminAccount: string;
-    /**
-     * 租户域名
-     * @type {string}
-     * @memberof AddTenantInput
-     */
-    host: string;
     /**
      * 系统主标题
      * @type {string}
@@ -213,12 +207,6 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     viceDesc: string;
-    /**
-     * 水印内容
-     * @type {string}
-     * @memberof AddTenantInput
-     */
-    watermark: string;
     /**
      * 版权说明
      * @type {string}

+ 57 - 0
Web/src/api-services/models/admin-result-list-backup-output.ts

@@ -0,0 +1,57 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { BackupOutput } from './backup-output';
+/**
+ * 全局返回结果
+ * @export
+ * @interface AdminResultListBackupOutput
+ */
+export interface AdminResultListBackupOutput {
+    /**
+     * 状态码
+     * @type {number}
+     * @memberof AdminResultListBackupOutput
+     */
+    code?: number;
+    /**
+     * 类型success、warning、error
+     * @type {string}
+     * @memberof AdminResultListBackupOutput
+     */
+    type?: string | null;
+    /**
+     * 错误信息
+     * @type {string}
+     * @memberof AdminResultListBackupOutput
+     */
+    message?: string | null;
+    /**
+     * 数据
+     * @type {Array<BackupOutput>}
+     * @memberof AdminResultListBackupOutput
+     */
+    result?: Array<BackupOutput> | null;
+    /**
+     * 附加数据
+     * @type {any}
+     * @memberof AdminResultListBackupOutput
+     */
+    extras?: any | null;
+    /**
+     * 时间
+     * @type {Date}
+     * @memberof AdminResultListBackupOutput
+     */
+    time?: Date;
+}

+ 32 - 0
Web/src/api-services/models/backup-output.ts

@@ -0,0 +1,32 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+/**
+ * 
+ * @export
+ * @interface BackupOutput
+ */
+export interface BackupOutput {
+    /**
+     * 文件名
+     * @type {string}
+     * @memberof BackupOutput
+     */
+    fileName?: string | null;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof BackupOutput
+     */
+    createTime?: Date;
+}

+ 6 - 0
Web/src/api-services/models/enum-type-output.ts

@@ -30,6 +30,12 @@ export interface EnumTypeOutput {
      * @memberof EnumTypeOutput
      */
     typeName?: string | null;
+    /**
+     * 枚举类型全名称
+     * @type {string}
+     * @memberof EnumTypeOutput
+     */
+    typeFullName?: string | null;
     /**
      * 枚举类型备注
      * @type {string}

+ 4 - 0
Web/src/api-services/models/index.ts

@@ -31,6 +31,7 @@ export * from './admin-result-int32';
 export * from './admin-result-int64';
 export * from './admin-result-jobject';
 export * from './admin-result-list-api-output';
+export * from './admin-result-list-backup-output';
 export * from './admin-result-list-code-gen-config';
 export * from './admin-result-list-column-ouput';
 export * from './admin-result-list-const-output';
@@ -115,6 +116,7 @@ export * from './admin-result-wx-open-id-output';
 export * from './admin-result-wx-phone-output';
 export * from './api-output';
 export * from './assembly';
+export * from './backup-output';
 export * from './base-id-input';
 export * from './base-proc-input';
 export * from './batch-config-input';
@@ -270,6 +272,7 @@ export * from './print-type-enum';
 export * from './property-attributes';
 export * from './property-info';
 export * from './reset-pwd-user-input';
+export * from './restore-input';
 export * from './role-input';
 export * from './role-menu-input';
 export * from './role-org-input';
@@ -401,6 +404,7 @@ export * from './user-role-input';
 export * from './visual-column';
 export * from './visual-db-table';
 export * from './visual-table';
+export * from './web-hook-input';
 export * from './wechat-pay-output';
 export * from './wechat-pay-page-input';
 export * from './wechat-pay-para-input';

+ 6 - 6
Web/src/api-services/models/info-save-input.ts

@@ -30,6 +30,12 @@ export interface InfoSaveInput {
      * @memberof InfoSaveInput
      */
     logoFileName?: string | null;
+    /**
+     * 水印内容
+     * @type {string}
+     * @memberof InfoSaveInput
+     */
+    watermark?: string | null;
     /**
      * 系统主标题
      * @type {string}
@@ -48,12 +54,6 @@ export interface InfoSaveInput {
      * @memberof InfoSaveInput
      */
     viceDesc: string;
-    /**
-     * 水印内容
-     * @type {string}
-     * @memberof InfoSaveInput
-     */
-    watermark: string;
     /**
      * 版权说明
      * @type {string}

+ 26 - 0
Web/src/api-services/models/restore-input.ts

@@ -0,0 +1,26 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+/**
+ * 还原输入参数
+ * @export
+ * @interface RestoreInput
+ */
+export interface RestoreInput {
+    /**
+     * 文件名
+     * @type {string}
+     * @memberof RestoreInput
+     */
+    fileName: string;
+}

+ 0 - 12
Web/src/api-services/models/tenant-output.ts

@@ -135,18 +135,6 @@ export interface TenantOutput {
      * @memberof TenantOutput
      */
     regWayId?: number | null;
-    /**
-     * 
-     * @type {YesNoEnum}
-     * @memberof TenantOutput
-     */
-    captcha?: YesNoEnum;
-    /**
-     * 
-     * @type {YesNoEnum}
-     * @memberof TenantOutput
-     */
-    secondVer?: YesNoEnum;
     /**
      * 图标
      * @type {string}

+ 11 - 23
Web/src/api-services/models/update-tenant-input.ts

@@ -87,6 +87,12 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     orgId?: number;
+    /**
+     * 域名
+     * @type {string}
+     * @memberof UpdateTenantInput
+     */
+    host?: string | null;
     /**
      * 
      * @type {TenantTypeEnum}
@@ -130,23 +136,17 @@ export interface UpdateTenantInput {
      */
     regWayId?: number | null;
     /**
-     * 
-     * @type {YesNoEnum}
-     * @memberof UpdateTenantInput
-     */
-    captcha?: YesNoEnum;
-    /**
-     * 
-     * @type {YesNoEnum}
+     * 图标
+     * @type {string}
      * @memberof UpdateTenantInput
      */
-    secondVer?: YesNoEnum;
+    logo: string;
     /**
-     * 图标
+     * 水印
      * @type {string}
      * @memberof UpdateTenantInput
      */
-    logo: string;
+    watermark?: string | null;
     /**
      * 排序
      * @type {number}
@@ -189,12 +189,6 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     adminAccount: string;
-    /**
-     * 租户域名
-     * @type {string}
-     * @memberof UpdateTenantInput
-     */
-    host: string;
     /**
      * 系统主标题
      * @type {string}
@@ -213,12 +207,6 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     viceDesc: string;
-    /**
-     * 水印内容
-     * @type {string}
-     * @memberof UpdateTenantInput
-     */
-    watermark: string;
     /**
      * 版权说明
      * @type {string}

+ 26 - 0
Web/src/api-services/models/web-hook-input.ts

@@ -0,0 +1,26 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+/**
+ * WebHook输入参数
+ * @export
+ * @interface WebHookInput
+ */
+export interface WebHookInput {
+    /**
+     * 密钥
+     * @type {string}
+     * @memberof WebHookInput
+     */
+    key: string;
+}

+ 269 - 0
Web/src/views/system/update/index.vue

@@ -0,0 +1,269 @@
+<template>
+	<div class="sys-update-container">
+		<el-container style="height: calc(100vh - 150px);">
+			<el-aside v-auth="'sysUpdate:list'" width="220px" class="backup-list">
+				<p class="backup-list-description">备份列表</p>
+				<el-scrollbar>
+					<div class="backup-items">
+						<div v-for="(backup, index) in state.backups" :key="index" class="backup-item" @mouseenter="hovered = index" @mouseleave="hovered = null">
+							<el-button type="text" :class="{ 'selected-backup': state.selectedBackup === backup, 'hovered-backup': hovered === index }" @click="() => state.selectedBackup = backup">
+								{{ backup.fileName }}
+							</el-button>
+						</div>
+					</div>
+				</el-scrollbar>
+			</el-aside>
+			<el-main v-auth="'sysUpdate:logs'" class="log-terminal-container">
+				<div class="toolbar">
+					<el-button-group>
+						<el-button v-auth="'sysUpdate:update'" :disabled="state.isUpdating" @click="handleAction('update')">更新</el-button>
+						<el-button v-auth="'sysUpdate:restore'" :disabled="!canRestore || state.isUpdating || !state.selectedBackup" @click="handleAction('restore')">还原</el-button>
+						<el-button v-auth="'sysUpdate:clear'" :disabled="state.isUpdating" @click="clearLogs">清空</el-button>
+						<el-button v-auth="'sysUpdate:webHookKey'" @click="getWebHookKey">获取密钥</el-button>
+					</el-button-group>
+				</div>
+				<div class="log-terminal">
+					<div class="terminal-output" ref="terminalOutput">
+						<pre>{{ state.logOutput }}</pre>
+					</div>
+				</div>
+			</el-main>
+		</el-container>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { reactive, computed, onMounted, nextTick, ref, onUnmounted } from 'vue';
+import { ElMessage, ElMessageBox } from "element-plus";
+import { getAPI } from "/@/utils/axios-utils";
+import { authAll } from "/@/utils/authFunction";
+import { BackupOutput, SysUpdateApi } from "/@/api-services";
+import commonFunction from "/@/utils/commonFunction";
+
+const { copyText } = commonFunction();
+const state = reactive({
+	selectedBackup: null as BackupOutput | null,
+	backups: [] as BackupOutput[],
+	isUpdating: false,
+	logOutput: '',
+});
+
+// 计算属性 canRestore
+const canRestore = computed(() => !!state.selectedBackup);
+
+// 引用元素
+const terminalOutput = ref<HTMLElement | null>(null);
+
+// 新增的悬停索引变量
+const hovered = ref<number | null>(null);
+
+let refreshInterval: number;
+
+// 获取初始数据
+const fetchData = async () => {
+	try {
+		state.backups = (await getAPI(SysUpdateApi).apiSysUpdateListPost()).data.result ?? [];
+		await refreshLog();
+	} catch (error) {
+		handleError('获取数据失败', error);
+	}
+};
+
+// 刷新日志
+const refreshLog = async () => {
+	try {
+		const response = await getAPI(SysUpdateApi).apiSysUpdateLogsGet();
+		state.logOutput = (response.data.result ?? []).join('\n');
+		scrollToBottom(); // 更新日志后立即滚动到底部
+	} catch (error) {
+		handleError('获取日志失败', error);
+	}
+};
+
+// 滚动到底部
+const scrollToBottom = () => {
+	nextTick(() => {
+		if (terminalOutput.value) {
+			terminalOutput.value.scrollTop = terminalOutput.value.scrollHeight;
+		}
+	});
+};
+
+// 启动/停止日志刷新定时器
+const toggleRefreshTimer = (start: boolean) => {
+	if (start && !refreshInterval) {
+		refreshInterval = window.setInterval(refreshLog, 300);
+	} else if (!start && refreshInterval) {
+		window.clearInterval(refreshInterval);
+		refreshInterval = 0;
+	}
+};
+
+// 处理动作
+const handleAction = async (action: 'update' | 'restore') => {
+	if (state.isUpdating) return;
+
+	state.isUpdating = true;
+	toggleRefreshTimer(true);
+
+	try {
+		switch (action) {
+			case 'update':
+				await getAPI(SysUpdateApi).apiSysUpdateUpdatePost({ timeout: -1 });
+				ElMessage.success('更新成功');
+				fetchData();
+				break;
+			case 'restore':
+				ElMessageBox.confirm(`确定要还原到 ${state.selectedBackup?.fileName} ?`, '提示', {
+					confirmButtonText: '确定',
+					cancelButtonText: '取消',
+					type: 'warning',
+				}).then(async () => {
+					await getAPI(SysUpdateApi).apiSysUpdateRestorePost({ fileName: state.selectedBackup?.fileName } as any);
+					ElMessage.success('还原成功');
+				});
+				break;
+		}
+	} catch (error) {
+		handleError(`执行${action}失败`, error);
+	} finally {
+		toggleRefreshTimer(false);
+		state.isUpdating = false;
+	}
+};
+
+// 清空日志
+const clearLogs = async () => {
+	try {
+		state.logOutput = '';
+		await getAPI(SysUpdateApi).apiSysUpdateClearGet();
+		ElMessage.success('日志已清空');
+	} catch (error) {
+		handleError('清空日志失败', error);
+	}
+};
+
+// 获取密钥
+const getWebHookKey = async () => {
+	try {
+		const res = await getAPI(SysUpdateApi).apiSysUpdateWebHookKeyGet();
+		if (res.data.result) copyText(res.data.result);
+	} catch (error) {
+		handleError('获取密钥失败', error);
+	}
+}
+
+// 错误处理
+const handleError = (message: string, error: any) => {
+	ElMessage.error(`${message},请稍后再试。`);
+};
+
+onMounted(() => {
+	if (!authAll(['sysUpdate:list', 'sysUpdate:logs'])) return;
+	fetchData();
+	toggleRefreshTimer(true);
+});
+
+onUnmounted(() => {
+	if (!authAll(['sysUpdate:list', 'sysUpdate:logs'])) return;
+	toggleRefreshTimer(false);
+});
+</script>
+
+<style scoped>
+
+.sys-update-container {
+	display: flex;
+	height: 100%;
+	background-color: #f0f2f5;
+}
+
+.backup-list-description {
+	margin-bottom: 10px;
+	color: #909399;
+}
+
+.backup-list {
+	background-color: #ffffff;
+	padding: 20px;
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	border-radius: 8px;
+	transition: box-shadow 0.3s ease-in-out;
+	max-height: 100%;
+	overflow: hidden;
+}
+
+.backup-items {
+	max-height: calc(100vh - 40px);
+	overflow-y: auto;
+	overflow-x: hidden;
+}
+
+.backup-item {
+	margin-bottom: 10px;
+	transition: transform 0.2s;
+}
+
+.backup-item:hover {
+	transform: translateX(5px);
+}
+
+.selected-backup, .hovered-backup {
+	font-weight: bold;
+	color: #409eff;
+}
+
+.action-button {
+	margin-top: 8px;
+	transition: background-color 0.3s;
+}
+
+.action-button:hover {
+	background-color: #ecf5ff;
+}
+
+.log-terminal-container {
+	flex-grow: 1;
+	padding: 20px;
+	display: flex;
+	flex-direction: column;
+	background-color: #ffffff;
+	border-radius: 8px;
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+	transition: box-shadow 0.3s ease-in-out;
+}
+
+.toolbar {
+	margin-bottom: 5px;
+	padding: 5px 10px 5px 10px;
+	background-color: #ffffff;
+	border-radius: 4px;
+	box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: flex-start;
+	gap: 8px;
+}
+
+.log-terminal {
+	background-color: #2c3e50;
+	color: #ecf0f1;
+	border-radius: 4px;
+	flex-grow: 1;
+	position: relative;
+	overflow: hidden;
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.terminal-output {
+	padding: 20px;
+	height: 100%;
+	overflow-y: auto;
+	white-space: pre-wrap;
+	word-wrap: break-word;
+	font-family: 'Courier New', Courier, monospace;
+	font-size: 14px;
+	line-height: 1.5;
+}
+</style>