Parcourir la source

新增通用导出功能

qinchaoyue il y a 3 mois
Parent
commit
4ffa2c0c8e

+ 157 - 9
src/Hotline.Api/Controllers/ExportData/ExportDataController.cs

@@ -1,23 +1,171 @@
-using Microsoft.AspNetCore.Components;
+using Hotline.Application.ExportExcel;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using MongoDB.Bson.IO;
+using SqlSugar;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
 
 namespace Hotline.Api.Controllers.ExportData;
 
 [ApiController]
-[Microsoft.AspNetCore.Mvc.Route("{*path:regex(.*export_data$)}")]
+[Route("{*path:regex(.*export_excel$)}")]
 public class ExportDataController : BaseController
 {
-    [HttpGet]
-    public IActionResult HandleExport()
+
+    private readonly IServiceProvider _serviceProvider;
+    private readonly EndpointDataSource _endpointDataSource;
+    private readonly IExportApplication _exportApplication;
+
+    public ExportDataController(IServiceProvider serviceProvider, EndpointDataSource endpointDataSource, IExportApplication exportApplication)
     {
-        // 处理 GET 请求
-        return Ok("This is a GET export request.");
+        _serviceProvider = serviceProvider;
+        _endpointDataSource = endpointDataSource;
+        _exportApplication = exportApplication;
     }
 
+    /// <summary>
+    /// 动态导出数据
+    /// </summary>
+    /// <returns></returns>
     [HttpPost]
-    public IActionResult HandleExportPost()
+    public async Task<FileStreamResult> HandleExportPost()
+    {
+        var fullPath = HttpContext.Request.Path.Value;
+        var originalPath = fullPath?.Substring(0, fullPath.LastIndexOf("/export_excel")).TrimStart('/');
+        if (string.IsNullOrEmpty(originalPath))
+            throw UserFriendlyException.SameMessage("Invalid export path.");
+
+        var matchingEndpoint = _endpointDataSource.Endpoints
+            .OfType<RouteEndpoint>()
+            .FirstOrDefault(endpoint => endpoint.RoutePattern.RawText == originalPath);
+
+        if (matchingEndpoint == null)
+            throw UserFriendlyException.SameMessage($"No route matches '{originalPath}'.");
+
+        var controllerName = matchingEndpoint.RoutePattern.RequiredValues["controller"]?.ToString();
+        var actionName = matchingEndpoint.RoutePattern.RequiredValues["action"]?.ToString();
+
+        var applicationServiceType = GetApplicationServiceType(controllerName);
+        if (applicationServiceType == null)
+            throw UserFriendlyException.SameMessage($"Application service for '{controllerName}' not found.");
+
+        var method = applicationServiceType.GetMethod(actionName);
+        if (method == null)
+        {
+            method = applicationServiceType.GetMethod(actionName + "Async");
+            if (method == null)
+                throw UserFriendlyException.SameMessage($"Action '{actionName}' not found in Application service.");
+        }
+
+        var serviceInstance = _serviceProvider.GetService(applicationServiceType);
+        if (serviceInstance == null)
+        {
+            serviceInstance = _serviceProvider.GetService(applicationServiceType.GetInterfaces()[0]);
+            if (serviceInstance == null)
+                throw UserFriendlyException.SameMessage($"Unable to create instance of Application service '{applicationServiceType.Name}'.");
+        }
+
+        try
+        {
+            var parameters = method.GetParameters();
+
+            using var reader = new StreamReader(HttpContext.Request.Body);
+            var body = await reader.ReadToEndAsync();
+            var param = parameters[0];
+            var genericType = typeof(ExportExcelDto<>).MakeGenericType(param.ParameterType);
+            var exportData = body.FromJson(genericType);
+            var queryDto = genericType.GetProperty("QueryDto")?.GetValue(exportData);
+            var isExportAll = genericType.GetProperty("IsExportAll")?.GetValue(exportData);
+            var pageIndex = param.ParameterType.GetProperty("PageIndex")?.GetValue(queryDto);
+            var pageSize = param.ParameterType.GetProperty("PageSize")?.GetValue(queryDto);
+            var result = method.Invoke(serviceInstance, [queryDto]);
+
+            var returnType = method.ReturnType.GetGenericArguments()[0];
+            var description = method.GetCustomAttribute<DescriptionAttribute>()?.Description;
+
+            return _exportApplication.GetExcelFile(returnType, genericType, exportData, ConvertToList(result, (bool)isExportAll, (int)pageIndex, (int)pageSize), description);
+
+        }
+        catch (Exception e)
+        {
+            var msg = e.Message;
+            throw;
+        }
+        throw UserFriendlyException.SameMessage("失败");
+    }
+
+    public static List<object>? ConvertToList(object? result, bool isExportAll, int pageIndex, int pageSize)
     {
-        // 处理 POST 请求
-        return Ok("This is a POST export request.");
+        if (result == null)
+        {
+            return null;
+        }
+
+        var type = result.GetType();
+
+        if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(PostgreSQLQueryable<>))
+        {
+            throw UserFriendlyException.SameMessage("被导出方法的返回类型不是 ISugarQueryable");
+        }
+
+        var genericArgument = type.GetGenericArguments()[0];
+
+        if (isExportAll)
+        {
+            var toListMethod = type.GetMethods()
+            .Where(m => m.Name == "ToList" && m.GetParameters().Length == 0)
+            .First();
+            var list = toListMethod.Invoke(result, null) as IEnumerable<object>;
+            return list?.ToList();
+        }
+        else
+        {
+            var toListMethod = type.GetMethods()
+            .Where(m => m.Name == "ToPageList" && m.GetParameters().Length == 2)
+            .First();
+            var list = toListMethod.Invoke(result, [pageIndex, pageSize]) as IEnumerable<object>;
+            return list?.ToList();
+        }
     }
+
+    private Type GetApplicationServiceType(string controllerName)
+    {
+        // 根据约定规则,将 Controller 映射到 Application 服务类
+        // 假设 Application 服务类命名规则为 "{ControllerName}Application"
+        var applicationServiceName = $"Hotline.Application.Snapshot.{controllerName}Application";
+        var name = controllerName + "Application";
+
+        var type = AppDomain.CurrentDomain.GetAssemblies().ToList()
+        .SelectMany(d => d.GetTypes())
+           .Where(d => d.Name == name)
+           .First();
+        return type;
+    }
+
+    private async Task<object[]> BindMethodParameters(System.Reflection.MethodInfo method)
+    {
+        // 动态解析方法的参数绑定
+        var parameters = method.GetParameters();
+        var result = new List<object>();
+
+        foreach (var param in parameters)
+        {
+            using var reader = new StreamReader(HttpContext.Request.Body);
+            var body = await reader.ReadToEndAsync();
+            var genericType = typeof(ExportExcelDto<>).MakeGenericType(param.ParameterType);
+            var deserializedObject = body.FromJson(genericType);
+            var queryDto = genericType.GetProperty("QueryDto")?.GetValue(deserializedObject);
+            result.Add(queryDto);
+        }
+
+        return result.ToArray();
+    }
+
 }

+ 17 - 0
src/Hotline.Api/Controllers/TestController.cs

@@ -73,6 +73,11 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
 using Order = Hotline.Orders.Order;
 using Hotline.Share.Dtos.Settings;
 using OrderDto = Hotline.Share.Dtos.Order.OrderDto;
+using Hotline.Share.Dtos.Home;
+using Google.Protobuf.WellKnownTypes;
+using Microsoft.AspNetCore.DataProtection;
+using Hotline.Share.Tools;
+using NETCore.Encrypt;
 
 namespace Hotline.Api.Controllers;
 
@@ -1342,4 +1347,16 @@ ICallApplication callApplication,
         }
     }
 
+    /// <summary>
+    /// 加密验证
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("check_token")]
+    [AllowAnonymous]
+    public async Task<string> CheckAES([FromBody] CheckTokenDto dto)
+    {
+        var strString = dto.AppId + dto.Timestamp;
+        return dto.AppId + EncryptProvider.AESEncrypt(strString, dto.Secret, dto.AppId);
+    }
 }

+ 1 - 0
src/Hotline.Api/Hotline.Api.csproj

@@ -16,6 +16,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.10" />
     <PackageReference Include="MiniExcel" Version="1.36.0" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
     <PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
     <PackageReference Include="Serilog.Sinks.MongoDB" Version="6.0.0" />

+ 3 - 3
src/Hotline.Api/StartupExtensions.cs

@@ -154,13 +154,13 @@ internal static class StartupExtensions
                 services.AddYbEnterpriseSdk(appConfiguration.YiBin.Enterprise.AddressUrl)
                     .AddKeyedScoped<ISessionContext, YbEnterpriseSessionContext>(YbEnterpriseSessionContext.Key)
                     .AddKeyedScoped<ISessionContext, ZzptSessionContext>(ZzptSessionContext.Key);
-                services.AddProxiedScoped<ISnapshotApplication, DefaultSnapshotApplication>();
+                //services.AddProxiedScoped<ISnapshotApplication, DefaultSnapshotApplication>();
                 break;
             case AppDefaults.AppScope.ZiGong:
-                services.AddProxiedScoped<ISnapshotApplication, ZiGongSnapshotApplication>();
+                //services.AddProxiedScoped<ISnapshotApplication, ZiGongSnapshotApplication>();
                 break;
             case AppDefaults.AppScope.LuZhou:
-                services.AddProxiedScoped<ISnapshotApplication, DefaultSnapshotApplication>();
+                //services.AddProxiedScoped<ISnapshotApplication, DefaultSnapshotApplication>();
                 break;
         }
 

+ 40 - 0
src/Hotline.Application.Tests/Application/ThirdIdentifyApplicationTest.cs

@@ -0,0 +1,40 @@
+using NETCore.Encrypt.Internal;
+using NETCore.Encrypt;
+using Org.BouncyCastle.Asn1.IsisMtt.X509;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NETCore.Encrypt.Extensions;
+using Hotline.Share.Tools;
+using System.Security.Cryptography;
+
+namespace Hotline.Application.Tests.Application;
+public class ThirdIdentifyApplicationTest
+{
+    [Fact]
+    public async Task ThirdSystem_Test()
+    {
+        var appId = "companyName";
+        var secret = "4x1q6YCWLDnHkpLTCWMwx3XQF7bA5QAd";
+
+
+        var iv = Guid.NewGuid().ToString().Substring(0, 16);
+
+
+        var unixTimespan = DateTimeOffset.Now.ToUnixTimeSeconds();
+        var strString = appId + unixTimespan;
+        var entrypted = EncryptProvider.AESEncrypt(strString, secret, iv);
+        var token = appId + iv + entrypted;
+
+        var decrypted = EncryptProvider.AESDecrypt(entrypted, secret, iv);
+        Console.WriteLine(decrypted);
+
+
+                                                        //"4x1q6YCWLDnHkpLTCWMwx3XQF7bA5QAd   WD7MEjbCySsniwKz
+        entrypted = EncryptProvider.AESEncrypt(strString, "NBdabUfdsabwB7382fdsab18v321udab");
+        decrypted = EncryptProvider.AESDecrypt(entrypted, "NBdabUfdsabwB7382fdsab18v321udab");
+        Console.WriteLine(decrypted);
+    }
+}

+ 1 - 0
src/Hotline.Application.Tests/Hotline.Application.Tests.csproj

@@ -26,6 +26,7 @@
     <PackageReference Include="coverlet.collector" Version="3.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.20" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.1" />
     <PackageReference Include="Senparc.Weixin" Version="6.19.1" />
     <PackageReference Include="Senparc.Weixin.AspNet" Version="1.3.1" />
     <PackageReference Include="Senparc.Weixin.WxOpen" Version="3.20.1" />

+ 20 - 0
src/Hotline.Application/ExportExcel/ExportApplication.cs

@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using MiniExcelLibs;
+using NPOI.HPSF;
 using System.Net.Http;
 using System.Reflection;
 using XF.Domain.Dependency;
@@ -67,6 +68,25 @@ namespace Hotline.Application.ExportExcel
             return GetExcelStream(dto, items, func).GetExcelFile(fileName);
         }
 
+        public FileStreamResult GetExcelFile(Type typeT, Type typeD, object dto, IList<object> items, string fileName)
+        {
+            var columnInfos = typeD.GetProperty("ColumnInfos")?.GetValue(dto) as List<ColumnInfo>;
+            if (columnInfos == null)
+            {
+                throw new ArgumentException("ColumnInfos not found in dto");
+            }
+
+            dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(columnInfos);
+
+            var dtos = items
+                .Select(item => _mapper.Map(item, typeT, dynamicClass))
+                .Cast<object>()
+                .ToList();
+
+            return ExcelHelper.CreateStream(dtos).GetExcelFile(fileName);
+        }
+
+
         public FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, string totalName) where T : new()
         {
             var fieldsAll = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

+ 2 - 0
src/Hotline.Application/ExportExcel/IExportApplication.cs

@@ -26,6 +26,8 @@ namespace Hotline.Application.ExportExcel
 
         FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items,string fileName, Func<IList<T>, T>? func = null);
 
+        FileStreamResult GetExcelFile(Type typeT, Type typeD, object dto, IList<object> items, string fileName);
+
         /// <summary>
         /// 导入数据
         /// </summary>

+ 7 - 0
src/Hotline.Application/Snapshot/RedPackApplication.cs

@@ -9,6 +9,7 @@ using Hotline.Snapshot.Interfaces;
 using Mapster;
 using SqlSugar;
 using SqlSugar.Extensions;
+using System.ComponentModel;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
@@ -508,6 +509,12 @@ public class RedPackApplication : IRedPackApplication, IScopeDependency
         return query;
     }
 
+    /// <summary>
+    /// 补充发放集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [Description("补充发放")]
     public ISugarQueryable<SnapshotRedPackRecordSupplementItemsOutDto> GetRedPackRecordSupplementItemsAsync(SnapshotRedPackRecordSupplementItemsInDto dto)
     {
         var query = _supplementRecordRepository.Queryable()

+ 24 - 0
src/Hotline.Share/Dtos/Home/CheckTokenDto.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Home;
+public class CheckTokenDto
+{
+    /// <summary>
+    /// AppId
+    /// </summary>
+    public string AppId { get; set; }
+
+    /// <summary>
+    /// Secret
+    /// </summary>
+    public string Secret { get; set; }
+
+    /// <summary>
+    /// timestamp
+    /// </summary>
+    public long Timestamp { get; set; }
+}

+ 6 - 1
src/Hotline.Share/Tools/ObjectExtensions.cs

@@ -75,7 +75,7 @@ public static class ObjectExtensions
     /// <typeparam name="T"></typeparam>
     /// <param name="model"></param>
     /// <param name="func">返回 true: contions; false: break</param>
-    public static void ForeachClassProperties<T>(this T model, Func<T, PropertyInfo , string, object, Task<bool>> func)
+    public static void ForeachClassProperties<T>(this T model, Func<T, PropertyInfo, string, object, Task<bool>> func)
     {
         var t = model.GetType();
         var propertyList = t.GetProperties();
@@ -103,4 +103,9 @@ public static class ObjectExtensions
     {
         return json.IsNullOrEmpty() ? default(T) : JsonConvert.DeserializeObject<T>(json);
     }
+
+    public static object? FromJson(this string json, Type returnType)
+    {
+        return json.IsNullOrEmpty() ? null : JsonConvert.DeserializeObject(json, returnType);
+    }
 }