Просмотр исходного кода

新增 友好异常AppException
新增 属性、字段、参数必填校验器ValidateRequiredAttribute
新增 动态api新增响应数据统一格式化,api响应数据格式化和swagger响应参数格式化
新增 appconfig.js新增动态api配置dynamicApi

zhontai 2 лет назад
Родитель
Сommit
d5920db666
31 измененных файлов с 591 добавлено и 81 удалено
  1. 8 3
      src/hosts/ZhonTai.Host/Configs/appconfig.json
  2. 11 0
      src/platform/ZhonTai.Admin/Core/Attributes/NonFormatResultAttribute.cs
  3. 6 2
      src/platform/ZhonTai.Admin/Core/Auth/ResponseAuthenticationHandler.cs
  4. 16 0
      src/platform/ZhonTai.Admin/Core/Configs/AppConfig.cs
  5. 13 1
      src/platform/ZhonTai.Admin/Core/Dto/ResultOutput.cs
  6. 31 0
      src/platform/ZhonTai.Admin/Core/Exceptions/Entity.cs
  7. 4 3
      src/platform/ZhonTai.Admin/Core/Filters/ControllerLogFilter.cs
  8. 67 0
      src/platform/ZhonTai.Admin/Core/Filters/FormatResultFilter.cs
  9. 16 12
      src/platform/ZhonTai.Admin/Core/HostApp.cs
  10. 60 0
      src/platform/ZhonTai.Admin/Core/Middlewares/ExceptionMiddleware.cs
  11. 34 0
      src/platform/ZhonTai.Admin/Core/Validators/ValidateRequiredAttribute.cs
  12. 11 17
      src/platform/ZhonTai.Admin/Services/Api/ApiService.cs
  13. 6 1
      src/platform/ZhonTai.Admin/Services/Api/Dto/ApiUpdateInput.cs
  14. 7 5
      src/platform/ZhonTai.Admin/Services/Api/IApiService.cs
  15. 19 26
      src/platform/ZhonTai.Admin/Services/Auth/AuthService.cs
  16. 14 0
      src/platform/ZhonTai.Admin/Services/Auth/Dto/AuthGetPasswordEncryptKeyOutput.cs
  17. 8 6
      src/platform/ZhonTai.Admin/Services/Auth/IAuthService.cs
  18. 3 3
      src/platform/ZhonTai.Admin/ZhonTai.Admin.csproj
  19. 57 0
      src/platform/ZhonTai.Admin/ZhonTai.Admin.xml
  20. 7 0
      src/platform/ZhonTai.Common/Extensions/MethodInfoExtension.cs
  21. 50 0
      src/platform/ZhonTai.Common/Helpers/JsonHelper.cs
  22. 29 0
      src/platform/ZhonTai.Common/ZhonTai.Common.xml
  23. 4 0
      src/platform/ZhonTai.DynamicApi/AppConsts.cs
  24. 32 0
      src/platform/ZhonTai.DynamicApi/Attributes/FormatResultAttribute.cs
  25. 17 0
      src/platform/ZhonTai.DynamicApi/DynamicApiConvention.cs
  26. 4 0
      src/platform/ZhonTai.DynamicApi/DynamicApiOptions.cs
  27. 2 1
      src/platform/ZhonTai.DynamicApi/DynamicApiServiceExtensions.cs
  28. 22 0
      src/platform/ZhonTai.DynamicApi/Extensions/MethodInfoExtension.cs
  29. 8 0
      src/platform/ZhonTai.DynamicApi/Response/FormatResultContext.cs
  30. 24 0
      src/platform/ZhonTai.DynamicApi/Response/ResponseResul.cs
  31. 1 1
      src/tests/ZhonTai.Tests/Services/ApiServiceTest.cs

+ 8 - 3
src/hosts/ZhonTai.Host/Configs/appconfig.json

@@ -30,7 +30,7 @@
       {
         "name": "中台Admin",
         "code": "admin",
-        "version": "v2.2.1",
+        "version": "v2.2.23",
         "description": ""
       }
     ]
@@ -71,10 +71,15 @@
   //验证码
   "varifyCode": {
     //启用
-    "enable": true,
+    "enable": false,
     //字体列表
     "fonts": [ "Times New Roman", "Verdana", "Arial", "Gungsuh", "Impact" ]
   },
   //默认密码
-  "defaultPassword": "111111"
+  "defaultPassword": "111111",
+  //动态api
+  "dynamicApi": {
+    //结果格式化
+    "formatResult": true
+  }
 }

+ 11 - 0
src/platform/ZhonTai.Admin/Core/Attributes/NonFormatResultAttribute.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace ZhonTai.Admin.Core.Attributes;
+
+/// <summary>
+/// 不格式化结果数据
+/// </summary>
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
+public class NonFormatResultAttribute : Attribute
+{
+}

+ 6 - 2
src/platform/ZhonTai.Admin/Core/Auth/ResponseAuthenticationHandler.cs

@@ -39,7 +39,8 @@ public class ResponseAuthenticationHandler : AuthenticationHandler<Authenticatio
             new ResponseStatusData
             {
                 Code = StatusCodes.Status401Unauthorized,
-                Msg = StatusCodes.Status401Unauthorized.ToDescription()
+                Msg = StatusCodes.Status401Unauthorized.ToDescription(),
+                Success = false
             },
             new JsonSerializerSettings()
             {
@@ -56,7 +57,8 @@ public class ResponseAuthenticationHandler : AuthenticationHandler<Authenticatio
             new ResponseStatusData
             {
                 Code = StatusCodes.Status403Forbidden,
-                Msg = StatusCodes.Status403Forbidden.ToDescription()
+                Msg = StatusCodes.Status403Forbidden.ToDescription(),
+                Success = false
             },
             new JsonSerializerSettings()
             {
@@ -70,4 +72,6 @@ public class ResponseStatusData
 {
     public StatusCodes Code { get; set; } = StatusCodes.Status1Ok;
     public string Msg { get; set; }
+    public bool Success { get; set; }
+
 }

+ 16 - 0
src/platform/ZhonTai.Admin/Core/Configs/AppConfig.cs

@@ -84,6 +84,11 @@ public class AppConfig
     /// 默认密码
     /// </summary>
     public string DefaultPassword { get; set; } = "111111";
+
+    /// <summary>
+    /// 动态Api配置
+    /// </summary>
+    public DynamicApiConfig DynamicApi { get; set; } = new DynamicApiConfig();
 }
 
 /// <summary>
@@ -246,6 +251,17 @@ public class ProjectConfig
     public string Description { get; set; }
 }
 
+/// <summary>
+/// 动态api配置
+/// </summary>
+public class DynamicApiConfig
+{
+    /// <summary>
+    /// 结果格式化
+    /// </summary>
+    public bool FormatResult { get; set; } = true;
+}
+
 /// <summary>
 /// 应用程序类型
 /// </summary>

+ 13 - 1
src/platform/ZhonTai.Admin/Core/Dto/ResultOutput.cs

@@ -1,4 +1,6 @@
-namespace ZhonTai.Admin.Core.Dto;
+using ZhonTai.Admin.Core.Exceptions;
+
+namespace ZhonTai.Admin.Core.Dto;
 
 /// <summary>
 /// 结果输出
@@ -101,6 +103,16 @@ public static partial class ResultOutput
         return new ResultOutput<string>().NotOk(msg);
     }
 
+    /// <summary>
+    /// 系统异常
+    /// </summary>
+    /// <param name="msg">消息</param>
+    /// <returns></returns>
+    public static AppException Exception(string msg = null)
+    {
+        return new AppException(msg);
+    }
+
     /// <summary>
     /// 根据布尔值返回结果
     /// </summary>

+ 31 - 0
src/platform/ZhonTai.Admin/Core/Exceptions/Entity.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace ZhonTai.Admin.Core.Exceptions;
+
+/// <summary>
+/// 系统异常
+/// </summary>
+public class AppException : Exception
+{
+    public AppException()
+    {
+    }
+
+    public AppException(SerializationInfo serializationInfo, StreamingContext context)
+        : base(serializationInfo, context)
+    {
+    }
+
+    public AppException(string message)
+        : base(message)
+    {
+    }
+
+
+    public AppException(string message, Exception innerException)
+        : base(message, innerException)
+    {
+    }
+}
+    

+ 4 - 3
src/platform/ZhonTai.Admin/Core/Filters/ControllerLogFilter.cs

@@ -18,13 +18,14 @@ public class ControllerLogFilter : IAsyncActionFilter
         _logHandler = logHandler;
     }
 
-    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
     {
         if (context.ActionDescriptor.EndpointMetadata.Any(m => m.GetType() == typeof(NoOprationLogAttribute)))
         {
-            return next();
+            await next();
+            return;
         }
 
-        return _logHandler.LogAsync(context, next);
+        await _logHandler.LogAsync(context, next);
     }
 }

+ 67 - 0
src/platform/ZhonTai.Admin/Core/Filters/FormatResultFilter.cs

@@ -0,0 +1,67 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.Linq;
+using System.Threading.Tasks;
+using ZhonTai.Admin.Core.Attributes;
+using ZhonTai.Admin.Core.Dto;
+
+namespace ZhonTai.Admin.Core.Filters;
+
+/// <summary>
+/// 结果格式化过滤器
+/// </summary>
+public class FormatResultFilter : IAsyncActionFilter
+{
+    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+    {
+        var actionExecutionContext = await next();
+        
+        if (actionExecutionContext.Exception != null)
+        {
+            return;
+        }
+
+        if (context.ActionDescriptor.EndpointMetadata.Any(m => m.GetType() == typeof(NonFormatResultAttribute)))
+        {
+            return;
+        }
+
+        IActionResult result = actionExecutionContext.Result;
+        
+        var formatResult = result switch
+        {
+            ViewResult => false,
+            PartialViewResult => false,
+            ViewComponentResult => false,
+            PageResult => false,
+            FileResult => false,
+            SignInResult => false,
+            SignOutResult => false,
+            RedirectToPageResult => false,
+            RedirectToRouteResult => false,
+            RedirectResult => false,
+            RedirectToActionResult => false,
+            LocalRedirectResult => false,
+            ChallengeResult => false,
+            ForbidResult => false,
+            BadRequestObjectResult => false,
+            _ => true,
+        };
+
+        if (!formatResult)
+        {
+            return;
+        }
+
+        var data = result switch
+        {
+            ContentResult contentResult => contentResult.Content,
+            ObjectResult objectResult => objectResult.Value,
+            JsonResult jsonResult => jsonResult.Value,
+            _ => null,
+        };
+
+        actionExecutionContext.Result = new JsonResult(new ResultOutput<dynamic>().Ok(data));
+    }
+}

+ 16 - 12
src/platform/ZhonTai.Admin/Core/HostApp.cs

@@ -48,6 +48,8 @@ using ZhonTai.Admin.Core.Startup;
 using ZhonTai.Admin.Core.Conventions;
 using FreeSql;
 using ZhonTai.Admin.Services.User;
+using ZhonTai.Admin.Core.Middlewares;
+using ZhonTai.Admin.Core.Dto;
 
 namespace ZhonTai.Admin.Core;
 
@@ -421,9 +423,9 @@ public class HostApp
         #region 控制器
         void mvcConfigure(MvcOptions options)
         {
-            options.Filters.Add<ControllerExceptionFilter>();
+            //options.Filters.Add<ControllerExceptionFilter>();
             options.Filters.Add<ValidateInputFilter>();
-            if(appConfig.Validate.Login || appConfig.Validate.Permission)
+            if (appConfig.Validate.Login || appConfig.Validate.Permission)
             {
                 options.Filters.Add<ValidatePermissionAttribute>();
             }
@@ -431,6 +433,11 @@ public class HostApp
             {
                 options.Filters.Add<ControllerLogFilter>();
             }
+            if (appConfig.DynamicApi.FormatResult)
+            {
+                options.Filters.Add<FormatResultFilter>();
+            }
+
             //禁止去除ActionAsync后缀
             //options.SuppressAsyncSuffixInActionNames = false;
 
@@ -518,6 +525,9 @@ public class HostApp
             .Select(o => Assembly.Load(new AssemblyName(o.Name))).ToArray();
             options.AddAssemblyOptions(assemblies);
 
+            options.FormatResult = appConfig.DynamicApi.FormatResult;
+            options.FormatResultType = typeof(ResultOutput<>);
+
             _hostAppOptions?.ConfigureDynamicApi?.Invoke(options);
         });
 
@@ -542,6 +552,9 @@ public class HostApp
 
         _hostAppOptions?.ConfigurePreMiddleware?.Invoke(hostAppMiddlewareContext);
 
+        //异常处理
+        app.UseMiddleware<ExceptionMiddleware>();
+
         //IP限流
         if (appConfig.RateLimit)
         {
@@ -554,9 +567,6 @@ public class HostApp
             app.UseMiniProfiler();
         }
 
-        //异常
-        app.UseExceptionHandler("/Error");
-
         //静态文件
         app.UseDefaultFiles();
         app.UseStaticFiles();
@@ -574,7 +584,7 @@ public class HostApp
         //授权
         app.UseAuthorization();
 
-        //初始化登录用户数据权限
+        //登录用户初始化数据权限
         app.Use(async (ctx, next) =>
         {
             var user = ctx.RequestServices.GetRequiredService<IUser>();
@@ -627,12 +637,6 @@ public class HostApp
         }
         #endregion Swagger Api文档
 
-        //数据库日志
-        //var log = LogManager.GetLogger("db");
-        //var ei = new LogEventInfo(LogLevel.Error, "", "错误信息");
-        //ei.Properties["id"] = YitIdHelper.NextId();
-        //log.Log(ei);
-
         _hostAppOptions?.ConfigurePostMiddleware?.Invoke(hostAppMiddlewareContext);
     }
 }

+ 60 - 0
src/platform/ZhonTai.Admin/Core/Middlewares/ExceptionMiddleware.cs

@@ -0,0 +1,60 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using System.Net;
+using System;
+using System.Threading.Tasks;
+using ZhonTai.Admin.Core.Exceptions;
+using ZhonTai.Admin.Core.Dto;
+using ZhonTai.Common.Helpers;
+
+namespace ZhonTai.Admin.Core.Middlewares;
+
+/// <summary>
+/// 异常中间件
+/// </summary>
+public class ExceptionMiddleware
+{
+    private readonly RequestDelegate _next;
+    private readonly ILogger _logger;
+
+    public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
+    {
+        _next = next;
+        _logger = logger;
+    }
+
+    public async Task InvokeAsync(HttpContext httpContext)
+    {
+        try
+        {
+            await _next(httpContext);
+        }
+        catch (AppException ex)
+        {
+            await HandleAppExceptionAsync(httpContext, ex);
+        }
+        catch (Exception ex)
+        {
+            await HandleExceptionAsync(httpContext, ex);
+        }
+    }
+
+    private static Task HandleAppExceptionAsync(HttpContext context, Exception exception)
+    {
+        context.Response.ContentType = "application/json";
+        context.Response.StatusCode = (int)HttpStatusCode.OK;
+
+        //_logger.LogError(exception, "");
+
+        return context.Response.WriteAsync(JsonHelper.Serialize(new ResultOutput<string>().NotOk(exception.Message)));
+    }
+
+    private Task HandleExceptionAsync(HttpContext context, Exception exception)
+    {
+        context.Response.ContentType = "application/json";
+        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+        _logger.LogError(exception, "");
+
+        return context.Response.WriteAsync(JsonHelper.Serialize(new ResultOutput<string>().NotOk(exception.Message)));
+    }
+}

+ 34 - 0
src/platform/ZhonTai.Admin/Core/Validators/ValidateRequiredAttribute.cs

@@ -0,0 +1,34 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace ZhonTai.Admin.Core.Validators
+{
+    /// <summary>
+    /// 指定属性、字段、参数必填
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
+    public class ValidateRequiredAttribute : ValidationAttribute
+    {
+        public ValidateRequiredAttribute() : base("{0} 为必填项") { }
+
+        public ValidateRequiredAttribute(string errorMessage) : base(errorMessage) { }
+
+        public override bool IsValid(object value)
+        {
+            if (value is null)
+            {
+                return false;
+            }
+
+            var valid  = value switch
+            {
+                Guid guid => guid != Guid.Empty,
+                long longValue => longValue > 0,
+                int intValue => intValue > 0,
+                _ => true
+            };
+
+            return valid;
+        }
+    }
+}

+ 11 - 17
src/platform/ZhonTai.Admin/Services/Api/ApiService.cs

@@ -31,10 +31,10 @@ public class ApiService : BaseService, IApiService, IDynamicApi
     /// </summary>
     /// <param name="id"></param>
     /// <returns></returns>
-    public async Task<IResultOutput> GetAsync(long id)
+    public async Task<ApiGetOutput> GetAsync(long id)
     {
         var result = await _apiRepository.GetAsync<ApiGetOutput>(id);
-        return ResultOutput.Ok(result);
+        return result;
     }
 
     /// <summary>
@@ -42,13 +42,13 @@ public class ApiService : BaseService, IApiService, IDynamicApi
     /// </summary>
     /// <param name="key"></param>
     /// <returns></returns>
-    public async Task<IResultOutput> GetListAsync(string key)
+    public async Task<List<ApiListOutput>> GetListAsync(string key)
     {
         var data = await _apiRepository
             .WhereIf(key.NotNull(), a => a.Path.Contains(key) || a.Label.Contains(key))
             .ToListAsync<ApiListOutput>();
 
-        return ResultOutput.Ok(data);
+        return data;
     }
 
     /// <summary>
@@ -57,7 +57,7 @@ public class ApiService : BaseService, IApiService, IDynamicApi
     /// <param name="input"></param>
     /// <returns></returns>
     [HttpPost]
-    public async Task<IResultOutput> GetPageAsync(PageInput<ApiGetPageDto> input)
+    public async Task<PageOutput<ApiEntity>> GetPageAsync(PageInput<ApiGetPageDto> input)
     {
         var key = input.Filter?.Label;
 
@@ -75,7 +75,7 @@ public class ApiService : BaseService, IApiService, IDynamicApi
             Total = total
         };
 
-        return ResultOutput.Ok(data);
+        return data;
     }
 
     /// <summary>
@@ -83,12 +83,12 @@ public class ApiService : BaseService, IApiService, IDynamicApi
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    public async Task<IResultOutput> AddAsync(ApiAddInput input)
+    public async Task<long> AddAsync(ApiAddInput input)
     {
         var entity = Mapper.Map<ApiEntity>(input);
-        var id = (await _apiRepository.InsertAsync(entity)).Id;
+        await _apiRepository.InsertAsync(entity);
 
-        return ResultOutput.Result(id > 0);
+        return entity.Id;
     }
 
     /// <summary>
@@ -96,22 +96,16 @@ public class ApiService : BaseService, IApiService, IDynamicApi
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    public async Task<IResultOutput> UpdateAsync(ApiUpdateInput input)
+    public async Task UpdateAsync(ApiUpdateInput input)
     {
-        if (!(input?.Id > 0))
-        {
-            return ResultOutput.NotOk();
-        }
-
         var entity = await _apiRepository.GetAsync(input.Id);
         if (!(entity?.Id > 0))
         {
-            return ResultOutput.NotOk("接口不存在!");
+            throw ResultOutput.Exception("接口不存在!");
         }
 
         Mapper.Map(input, entity);
         await _apiRepository.UpdateAsync(entity);
-        return ResultOutput.Ok();
     }
 
     /// <summary>

+ 6 - 1
src/platform/ZhonTai.Admin/Services/Api/Dto/ApiUpdateInput.cs

@@ -1,4 +1,7 @@
-namespace ZhonTai.Admin.Services.Api.Dto;
+using System.ComponentModel.DataAnnotations;
+using ZhonTai.Admin.Core.Validators;
+
+namespace ZhonTai.Admin.Services.Api.Dto;
 
 /// <summary>
 /// 修改
@@ -8,5 +11,7 @@ public partial class ApiUpdateInput : ApiAddInput
     /// <summary>
     /// 接口Id
     /// </summary>
+    [Required]
+    [ValidateRequired("请选择接口")]
     public long Id { get; set; }
 }

+ 7 - 5
src/platform/ZhonTai.Admin/Services/Api/IApiService.cs

@@ -2,6 +2,8 @@
 using System.Threading.Tasks;
 using ZhonTai.Admin.Services.Api.Dto;
 using ZhonTai.Admin.Domain.Api.Dto;
+using System.Collections.Generic;
+using ZhonTai.Admin.Domain.Api;
 
 namespace ZhonTai.Admin.Services.Api;
 
@@ -10,15 +12,15 @@ namespace ZhonTai.Admin.Services.Api;
 /// </summary>
 public interface IApiService
 {
-    Task<IResultOutput> GetAsync(long id);
+    Task<ApiGetOutput> GetAsync(long id);
 
-    Task<IResultOutput> GetListAsync(string key);
+    Task<List<ApiListOutput>> GetListAsync(string key);
 
-    Task<IResultOutput> GetPageAsync(PageInput<ApiGetPageDto> input);
+    Task<PageOutput<ApiEntity>> GetPageAsync(PageInput<ApiGetPageDto> input);
 
-    Task<IResultOutput> AddAsync(ApiAddInput input);
+    Task<long> AddAsync(ApiAddInput input);
 
-    Task<IResultOutput> UpdateAsync(ApiUpdateInput input);
+    Task UpdateAsync(ApiUpdateInput input);
 
     Task<IResultOutput> DeleteAsync(long id);
 

+ 19 - 26
src/platform/ZhonTai.Admin/Services/Auth/AuthService.cs

@@ -98,30 +98,26 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
     [HttpGet]
     [AllowAnonymous]
     [NoOprationLog]
-    public async Task<IResultOutput> GetPasswordEncryptKeyAsync()
+    public async Task<AuthGetPasswordEncryptKeyOutput> GetPasswordEncryptKeyAsync()
     {
         //写入Redis
         var guid = Guid.NewGuid().ToString("N");
         var key = CacheKeys.PassWordEncrypt + guid;
         var encyptKey = StringHelper.GenerateRandom(8);
         await Cache.SetAsync(key, encyptKey, TimeSpan.FromMinutes(5));
-        var data = new { key = guid, encyptKey };
-
-        return ResultOutput.Ok(data);
+        return new AuthGetPasswordEncryptKeyOutput { Key = guid, EncyptKey = encyptKey };
     }
 
-   
-
     /// <summary>
     /// 查询用户信息
     /// </summary>
     /// <returns></returns>
     [Login]
-    public async Task<IResultOutput> GetUserInfoAsync()
+    public async Task<AuthGetUserInfoOutput> GetUserInfoAsync()
     {
         if (!(User?.Id > 0))
         {
-            return ResultOutput.NotOk("未登录");
+            throw ResultOutput.Exception("未登录");
         }
 
         using (_userRepository.DataFilter.Disable(FilterNames.Self, FilterNames.Data))
@@ -182,7 +178,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
             //用户权限点
             authGetUserInfoOutput.Permissions = await dotSelect.ToListAsync(a => a.Code);
 
-            return ResultOutput.Ok(authGetUserInfoOutput);
+            return authGetUserInfoOutput;
         }
     }
 
@@ -194,7 +190,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
     [HttpPost]
     [AllowAnonymous]
     [NoOprationLog]
-    public async Task<IResultOutput> LoginAsync(AuthLoginInput input)
+    public async Task<dynamic> LoginAsync(AuthLoginInput input)
     {
         using (_userRepository.DataFilter.Disable(FilterNames.Tenant, FilterNames.Self, FilterNames.Data))
         {
@@ -210,7 +206,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
                 var isOk = await _captchaTool.CheckAsync(input.Captcha);
                 if (!isOk)
                 {
-                    return ResultOutput.NotOk("安全验证不通过,请重新登录");
+                    throw ResultOutput.Exception("安全验证不通过,请重新登录");
                 }
             }
 
@@ -227,14 +223,14 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
                     var secretKey = await Cache.GetAsync(passwordEncryptKey);
                     if (secretKey.IsNull())
                     {
-                        return ResultOutput.NotOk("解密失败");
+                        throw ResultOutput.Exception("解密失败");
                     }
                     input.Password = DesEncrypt.Decrypt(input.Password, secretKey);
                     await Cache.DelAsync(passwordEncryptKey);
                 }
                 else
                 {
-                    return ResultOutput.NotOk("解密失败!");
+                    throw ResultOutput.Exception("解密失败!");
                 }
             }
 
@@ -246,12 +242,12 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
 
             if (!(user?.Id > 0))
             {
-                return ResultOutput.NotOk("用户名或密码错误");
+                throw ResultOutput.Exception("用户名或密码错误");
             }
 
             if (user.Status == UserStatus.Disabled)
             {
-                return ResultOutput.NotOk("禁止登录,请联系管理员");
+                throw ResultOutput.Exception("禁止登录,请联系管理员");
             }
             #endregion
 
@@ -284,7 +280,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
 
             #endregion 添加登录日志
 
-            return ResultOutput.Ok(new { token });
+            return new { token };
         }
     }
 
@@ -296,7 +292,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
     /// <returns></returns>
     [HttpGet]
     [AllowAnonymous]
-    public async Task<IResultOutput> Refresh([BindRequired] string token)
+    public async Task<dynamic> Refresh([BindRequired] string token)
     {
         var jwtSecurityToken = LazyGetRequiredService<IUserToken>().Decode(token);
         var userClaims = jwtSecurityToken?.Claims?.ToArray();
@@ -333,7 +329,7 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
 
         var output = await LazyGetRequiredService<IUserService>().GetLoginUserAsync(userId.ToLong());
         string newToken = GetToken(output?.Data);
-        return ResultOutput.Ok(new { token = newToken });
+        return new { token = newToken };
     }
 
     /// <summary>
@@ -344,13 +340,10 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
     [AllowAnonymous]
     [NoOprationLog]
     [EnableCors(AdminConsts.AllowAnyPolicyName)]
-    public async Task<IResultOutput> GetCaptcha()
+    public async Task<CaptchaOutput> GetCaptcha()
     {
-        using (MiniProfiler.Current.Step("获取滑块验证"))
-        {
-            var data = await _captchaTool.GetAsync(CacheKeys.Captcha);
-            return ResultOutput.Ok(data);
-        }
+        var data = await _captchaTool.GetAsync(CacheKeys.Captcha);
+        return data;
     }
 
     /// <summary>
@@ -361,10 +354,10 @@ public class AuthService : BaseService, IAuthService, IDynamicApi
     [AllowAnonymous]
     [NoOprationLog]
     [EnableCors(AdminConsts.AllowAnyPolicyName)]
-    public async Task<IResultOutput> CheckCaptcha([FromQuery] CaptchaInput input)
+    public async Task<bool> CheckCaptcha([FromQuery] CaptchaInput input)
     {
         input.CaptchaKey = CacheKeys.Captcha;
         var result = await _captchaTool.CheckAsync(input);
-        return ResultOutput.Result(result);
+        return result;
     }
 }

+ 14 - 0
src/platform/ZhonTai.Admin/Services/Auth/Dto/AuthGetPasswordEncryptKeyOutput.cs

@@ -0,0 +1,14 @@
+namespace ZhonTai.Admin.Services.Auth.Dto;
+
+public class AuthGetPasswordEncryptKeyOutput
+{
+    /// <summary>
+    /// 缓存键
+    /// </summary>
+    public string Key { get; set; }
+
+    /// <summary>
+    /// 密码加密密钥
+    /// </summary>
+    public string EncyptKey { get; set; }
+}

+ 8 - 6
src/platform/ZhonTai.Admin/Services/Auth/IAuthService.cs

@@ -1,4 +1,4 @@
-using ZhonTai.Admin.Core.Dto;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
 using System.Threading.Tasks;
 using ZhonTai.Admin.Services.Auth.Dto;
 using ZhonTai.Admin.Tools.Captcha;
@@ -10,14 +10,16 @@ namespace ZhonTai.Admin.Services.Auth;
 /// </summary>
 public interface IAuthService
 {
-    Task<IResultOutput> LoginAsync(AuthLoginInput input);
+    Task<dynamic> LoginAsync(AuthLoginInput input);
 
-    Task<IResultOutput> GetUserInfoAsync();
+    Task<AuthGetUserInfoOutput> GetUserInfoAsync();
 
-    Task<IResultOutput> GetPasswordEncryptKeyAsync();
+    Task<AuthGetPasswordEncryptKeyOutput> GetPasswordEncryptKeyAsync();
 
-    Task<IResultOutput> GetCaptcha();
+    Task<CaptchaOutput> GetCaptcha();
 
-    Task<IResultOutput> CheckCaptcha(CaptchaInput input);
+    Task<bool> CheckCaptcha(CaptchaInput input);
+
+    Task<dynamic> Refresh([BindRequired] string token);
 
 }

+ 3 - 3
src/platform/ZhonTai.Admin/ZhonTai.Admin.csproj

@@ -26,11 +26,11 @@
 		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.10" />
 		<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
 		<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
-		<PackageReference Include="NLog" Version="5.0.4" />
-		<PackageReference Include="NLog.Web.AspNetCore" Version="5.1.4" />
+		<PackageReference Include="NLog" Version="5.0.5" />
+		<PackageReference Include="NLog.Web.AspNetCore" Version="5.1.5" />
 		<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
 		<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
-		<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
+		<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.0" />
 		<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
 		<PackageReference Include="UAParser" Version="3.1.47" />
 		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />

+ 57 - 0
src/platform/ZhonTai.Admin/ZhonTai.Admin.xml

@@ -14,6 +14,11 @@
             启用登录
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Attributes.NonFormatResultAttribute">
+            <summary>
+            不格式化结果数据
+            </summary>
+        </member>
         <member name="T:ZhonTai.Admin.Core.Attributes.NoOprationLogAttribute">
             <summary>
             禁用操作日志
@@ -346,6 +351,11 @@
             默认密码
             </summary>
         </member>
+        <member name="P:ZhonTai.Admin.Core.Configs.AppConfig.DynamicApi">
+            <summary>
+            动态Api配置
+            </summary>
+        </member>
         <member name="T:ZhonTai.Admin.Core.Configs.SwaggerConfig">
             <summary>
             Swagger配置
@@ -491,6 +501,16 @@
             描述
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Configs.DynamicApiConfig">
+            <summary>
+            动态api配置
+            </summary>
+        </member>
+        <member name="P:ZhonTai.Admin.Core.Configs.DynamicApiConfig.FormatResult">
+            <summary>
+            结果格式化
+            </summary>
+        </member>
         <member name="T:ZhonTai.Admin.Core.Configs.AppType">
             <summary>
             应用程序类型
@@ -1088,6 +1108,13 @@
             <param name="msg">消息</param>
             <returns></returns>
         </member>
+        <member name="M:ZhonTai.Admin.Core.Dto.ResultOutput.Exception(System.String)">
+            <summary>
+            系统异常
+            </summary>
+            <param name="msg">消息</param>
+            <returns></returns>
+        </member>
         <member name="M:ZhonTai.Admin.Core.Dto.ResultOutput.Result``1(System.Boolean)">
             <summary>
             根据布尔值返回结果
@@ -1399,6 +1426,11 @@
             系统内部错误(非业务代码里显式抛出的异常,例如由于数据不正确导致空指针异常、数据库异常等等)
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Exceptions.AppException">
+            <summary>
+            系统异常
+            </summary>
+        </member>
         <member name="M:ZhonTai.Admin.Core.Extensions.RateLimitServiceCollectionExtensions.AddIpRateLimit(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.Extensions.Configuration.IConfiguration,ZhonTai.Admin.Tools.Cache.CacheConfig)">
             <summary>
             添加Ip限流
@@ -1417,6 +1449,11 @@
             控制器操作日志记录
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Filters.FormatResultFilter">
+            <summary>
+            结果格式化过滤器
+            </summary>
+        </member>
         <member name="T:ZhonTai.Admin.Core.Filters.ValidateInputFilter">
             <summary>
             输入模型验证过滤器
@@ -1498,6 +1535,11 @@
             操作日志处理
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Middlewares.ExceptionMiddleware">
+            <summary>
+            异常中间件
+            </summary>
+        </member>
         <member name="M:ZhonTai.Admin.Core.RegisterModules.ControllerModule.#ctor">
             <summary>
             控制器注入
@@ -1681,6 +1723,11 @@
             自定义数据库初始化
             </summary>
         </member>
+        <member name="T:ZhonTai.Admin.Core.Validators.ValidateRequiredAttribute">
+            <summary>
+            指定属性、字段、参数必填
+            </summary>
+        </member>
         <member name="T:ZhonTai.Admin.Domain.Api.ApiEntity">
             <summary>
             接口管理
@@ -3178,6 +3225,16 @@
             </summary>
             <returns></returns>
         </member>
+        <member name="P:ZhonTai.Admin.Services.Auth.Dto.AuthGetPasswordEncryptKeyOutput.Key">
+            <summary>
+            缓存键
+            </summary>
+        </member>
+        <member name="P:ZhonTai.Admin.Services.Auth.Dto.AuthGetPasswordEncryptKeyOutput.EncyptKey">
+            <summary>
+            密码加密密钥
+            </summary>
+        </member>
         <member name="P:ZhonTai.Admin.Services.Auth.Dto.AuthGetUserInfoOutput.User">
             <summary>
             用户个人信息

+ 7 - 0
src/platform/ZhonTai.Common/Extensions/MethodInfoExtension.cs

@@ -22,4 +22,11 @@ public static class MethodInfoExtension
         return method.ReturnType == typeof(Task)
             || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>));
     }
+
+    internal static Type GetReturnType(this MethodInfo method)
+    {
+        var isAsync = method.IsAsync();
+        var returnType = method.ReturnType;
+        return isAsync ? (returnType.GenericTypeArguments.FirstOrDefault() ?? typeof(void)) : returnType;
+    }
 }

+ 50 - 0
src/platform/ZhonTai.Common/Helpers/JsonHelper.cs

@@ -0,0 +1,50 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+
+namespace ZhonTai.Common.Helpers;
+
+/// <summary>
+/// Json帮助类
+/// </summary>
+public class JsonHelper
+{
+    private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
+    {
+        ContractResolver = new CamelCasePropertyNamesContractResolver(),
+        DateFormatString = "yyyy-MM-dd HH:mm:ss"
+};
+
+    /// <summary>
+    /// 序列化
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="obj"></param>
+    /// <returns></returns>
+    public static string Serialize<T>(T obj)
+    {
+        return JsonConvert.SerializeObject(obj, typeof(T), _jsonSerializerSettings);
+    }
+
+    /// <summary>
+    /// 反序列化
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="json"></param>
+    /// <returns></returns>
+    public static T Deserialize<T>(string json)
+    {
+        return JsonConvert.DeserializeObject<T>(json, _jsonSerializerSettings);
+    }
+
+    /// <summary>
+    /// 反序列化
+    /// </summary>
+    /// <param name="json">json文本</param>
+    /// <param name="type">类型</param>
+    /// <returns></returns>
+    public static object Deserialize(string json, Type type)
+    {
+        return JsonConvert.DeserializeObject(json, type, _jsonSerializerSettings);
+    }
+}

+ 29 - 0
src/platform/ZhonTai.Common/ZhonTai.Common.xml

@@ -408,6 +408,35 @@
             <param name="request"></param>
             <returns></returns>
         </member>
+        <member name="T:ZhonTai.Common.Helpers.JsonHelper">
+            <summary>
+            Json帮助类
+            </summary>
+        </member>
+        <member name="M:ZhonTai.Common.Helpers.JsonHelper.Serialize``1(``0)">
+            <summary>
+            序列化
+            </summary>
+            <typeparam name="T"></typeparam>
+            <param name="obj"></param>
+            <returns></returns>
+        </member>
+        <member name="M:ZhonTai.Common.Helpers.JsonHelper.Deserialize``1(System.String)">
+            <summary>
+            反序列化
+            </summary>
+            <typeparam name="T"></typeparam>
+            <param name="json"></param>
+            <returns></returns>
+        </member>
+        <member name="M:ZhonTai.Common.Helpers.JsonHelper.Deserialize(System.String,System.Type)">
+            <summary>
+            反序列化
+            </summary>
+            <param name="json">json文本</param>
+            <param name="type">类型</param>
+            <returns></returns>
+        </member>
         <member name="T:ZhonTai.Common.Helpers.MD5Encrypt">
             <summary>
             MD5加密

+ 4 - 0
src/platform/ZhonTai.DynamicApi/AppConsts.cs

@@ -28,6 +28,10 @@ public static class AppConsts
 
     public static Dictionary<Assembly, AssemblyDynamicApiOptions> AssemblyDynamicApiOptions { get; set; }
 
+    public static bool FormatResult { get; set; } = true;
+
+    public static Type FormatResultType { get; set; } = FormatResultContext.FormatResultType;
+
     static AppConsts()
     {
         HttpVerbs=new Dictionary<string, string>()

+ 32 - 0
src/platform/ZhonTai.DynamicApi/Attributes/FormatResultAttribute.cs

@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+
+namespace ZhonTai.DynamicApi.Attributes;
+
+[Serializable]
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+public class FormatResultAttribute : ProducesResponseTypeAttribute
+{
+    public FormatResultAttribute(int statusCode) : base(statusCode)
+    {
+    }
+
+    public FormatResultAttribute(Type type) : base(type, StatusCodes.Status200OK)
+    {
+        FormatType(type);
+    }
+
+    public FormatResultAttribute(Type type, int statusCode) : base(type, statusCode)
+    {
+        FormatType(type);
+    }
+
+    private void FormatType(Type type)
+    {
+        if (type != null && type != typeof(void))
+        {
+            Type = AppConsts.FormatResultType.MakeGenericType(type);
+        }
+    }
+}

+ 17 - 0
src/platform/ZhonTai.DynamicApi/DynamicApiConvention.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using System.Reflection;
 using System.Text.RegularExpressions;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ActionConstraints;
 using Microsoft.AspNetCore.Mvc.ApplicationModels;
@@ -142,8 +143,24 @@ public class DynamicApiConvention : IApplicationModelConvention
         ConfigureApiExplorer(controller);
         ConfigureSelector(controller, controllerAttr);
         ConfigureParameters(controller);
+        if (AppConsts.FormatResult)
+        {
+            ConfigureFormatResult(controller);
+        }
     }
 
+    private void ConfigureFormatResult(ControllerModel controller)
+    {
+        foreach (var action in controller.Actions)
+        {
+            if (!CheckNoMapMethod(action))
+            {
+                var returnType =  action.ActionMethod.GetReturnType();
+                if (returnType == typeof(void)) return;
+                action.Filters.Add(new FormatResultAttribute(returnType));
+            }
+        }
+    }
 
     private void ConfigureParameters(ControllerModel controller)
     {

+ 4 - 0
src/platform/ZhonTai.DynamicApi/DynamicApiOptions.cs

@@ -77,6 +77,10 @@ public class DynamicApiOptions
     public ISelectController SelectController { get; set; } = new DefaultSelectController();
     public IActionRouteFactory ActionRouteFactory { get; set; } = new DefaultActionRouteFactory();
 
+    public bool FormatResult { get; set; } = true;
+
+    public Type FormatResultType { get; set; } = FormatResultContext.FormatResultType;
+
     /// <summary>
     /// Verify that all configurations are valid
     /// </summary>

+ 2 - 1
src/platform/ZhonTai.DynamicApi/DynamicApiServiceExtensions.cs

@@ -101,8 +101,9 @@ public static class DynamicApiServiceExtensions
         AppConsts.NamingConvention = options.NamingConvention;
         AppConsts.GetRestFulControllerName = options.GetRestFulControllerName;
         AppConsts.GetRestFulActionName = options.GetRestFulActionName;
-
         AppConsts.AssemblyDynamicApiOptions = options.AssemblyDynamicApiOptions;
+        AppConsts.FormatResult = options.FormatResult;
+        AppConsts.FormatResultType = options.FormatResultType;
 
         var partManager = services.GetSingletonInstanceOrNull<ApplicationPartManager>();
 

+ 22 - 0
src/platform/ZhonTai.DynamicApi/Extensions/MethodInfoExtension.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace ZhonTai.DynamicApi;
+
+public static class MethodInfoExtension
+{
+    public static bool IsAsync(this MethodInfo method)
+    {
+        return method.ReturnType == typeof(Task)
+            || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>));
+    }
+
+    internal static Type GetReturnType(this MethodInfo method)
+    {
+        var isAsync = method.IsAsync();
+        var returnType = method.ReturnType;
+        return isAsync ? (returnType.GenericTypeArguments.FirstOrDefault() ?? typeof(void)) : returnType;
+    }
+}

+ 8 - 0
src/platform/ZhonTai.DynamicApi/Response/FormatResultContext.cs

@@ -0,0 +1,8 @@
+using System;
+
+namespace ZhonTai.DynamicApi;
+
+public static class FormatResultContext
+{
+    internal static Type FormatResultType = typeof(ResponseResul<>);
+}

+ 24 - 0
src/platform/ZhonTai.DynamicApi/Response/ResponseResul.cs

@@ -0,0 +1,24 @@
+namespace ZhonTai.DynamicApi;
+
+public class ResponseResul<T>
+{
+    /// <summary>
+    /// 是否成功标记
+    /// </summary>
+    public bool Success { get; private set; }
+
+    /// <summary>
+    /// 编码
+    /// </summary>
+    public string Code { get; set; }
+
+    /// <summary>
+    /// 消息
+    /// </summary>
+    public string Msg { get; private set; }
+
+    /// <summary>
+    /// 数据
+    /// </summary>
+    public T Data { get; private set; }
+}

+ 1 - 1
src/tests/ZhonTai.Tests/Services/ApiServiceTest.cs

@@ -27,6 +27,6 @@ public class ApiServiceTest : BaseTest
     public async void GetAsync()
     {
         var res = await _apiService.GetAsync(161227168079941);
-        Assert.True(res.Success);
+        Assert.True(res?.Id > 0);
     }
 }