From 5ac381f4f15b391b5b531fb2910a7775532762a7 Mon Sep 17 00:00:00 2001 From: xiaolipro <2357729423@qq.com> Date: Thu, 30 Oct 2025 17:20:08 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=201.=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B=E6=8E=A7=E5=88=B6=202.=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=BC=82=E5=B8=B8=E6=97=A5=E5=BF=97=203.=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81shopee=E5=AE=9A=E5=88=B6=E5=8C=96=204.=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8A=A8=E6=80=81=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InterfaceForwardApiModule.cs | 1 + src/InterfaceForward.Api/appsettings.json | 5 +- .../Dtos/Interface/ParameterParseInput.cs | 2 +- .../ForwardCore/ForwardCoreContext.cs | 5 ++ .../Filters/ExceptionNotifier.cs | 64 +++++++++++++- .../Filters/ForwardExceptionFilter.cs | 4 +- .../ForwardCore/DefaultForwardFlow.cs | 2 +- .../Services/App/AppsService.cs | 2 +- .../Services/Customized/Shopee.cs | 43 +++++++++ .../ShopeeForwardAuthorizeFilter.cs | 87 +++++++++++++++++++ .../ShopeeForwardExceptionFilter.cs | 79 +++++++++++++++++ .../Services/InterfaceForwardCommon.cs | 3 +- .../Services/InterfaceForwardService.cs | 7 +- .../Parameter/FixedParameterService.cs | 5 +- 14 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 src/InterfaceForward.Application/Services/Customized/Shopee.cs create mode 100644 src/InterfaceForward.Application/Services/Customized/ShopeeForwardAuthorizeFilter.cs create mode 100644 src/InterfaceForward.Application/Services/Customized/ShopeeForwardExceptionFilter.cs diff --git a/src/InterfaceForward.Api/InterfaceForwardApiModule.cs b/src/InterfaceForward.Api/InterfaceForwardApiModule.cs index e48d7ac..0a50729 100644 --- a/src/InterfaceForward.Api/InterfaceForwardApiModule.cs +++ b/src/InterfaceForward.Api/InterfaceForwardApiModule.cs @@ -5,6 +5,7 @@ using Fake.AspNetCore.Mvc.Filters; using Fake.Autofac; using Fake.Modularity; using InterfaceForward.Application; +using InterfaceForward.Application.Filters; using InterfaceForward.Domain.Shared.Helpers; namespace InterfaceForward.Api; diff --git a/src/InterfaceForward.Api/appsettings.json b/src/InterfaceForward.Api/appsettings.json index 6bf8e37..fa8dcb5 100644 --- a/src/InterfaceForward.Api/appsettings.json +++ b/src/InterfaceForward.Api/appsettings.json @@ -14,7 +14,10 @@ ], "WriteTo": [ { - "Name": "Console" + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}" + } } ] }, diff --git a/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterParseInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterParseInput.cs index a94b849..99e0063 100644 --- a/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterParseInput.cs +++ b/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterParseInput.cs @@ -8,7 +8,7 @@ namespace InterfaceForward.Application.Contracts.Dtos.Interface; public class ParameterParseInput { /// - /// 参数解析类型(0:json,1:xml) + /// 参数解析类型(1:json,2:xml) /// public ParameterParseTypeEnum ParameterParseType { get; set; } diff --git a/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs b/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs index ab61fb7..6b65a94 100644 --- a/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs +++ b/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs @@ -50,6 +50,11 @@ public class ForwardCoreContext /// 请求后置脚本 /// public string? PostfixScript { get; set; } + + /// + /// 返回参数 映射后参数类型(默认Object) + /// + public ParameterType MappedParamType { get; set; } public bool AddHeader(string key, string value) { diff --git a/src/InterfaceForward.Application/Filters/ExceptionNotifier.cs b/src/InterfaceForward.Application/Filters/ExceptionNotifier.cs index 075a461..ffa0b49 100644 --- a/src/InterfaceForward.Application/Filters/ExceptionNotifier.cs +++ b/src/InterfaceForward.Application/Filters/ExceptionNotifier.cs @@ -1,14 +1,70 @@ +using System.Text; using Fake.DependencyInjection; using Fake.ExceptionHandling; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace InterfaceForward.Application.Filters; -public class ExceptionNotifier: IExceptionNotifier, ITransientDependency +public class ExceptionNotifier : IExceptionNotifier, ITransientDependency { - public Task NotifyAsync(ExceptionNotificationContext context) + public async Task NotifyAsync(ExceptionNotificationContext context) { - LogHelper.Error(context.Exception.ToString()); + var httpContext = context.ServiceProvider.GetRequiredService().HttpContext; + if (httpContext is null) + { + LogHelper.Error($@" + |-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff} + |-异常堆栈:{context.Exception} "); + } + + var request = httpContext!.Request; + + // 1. 读取请求路径 + var path = request.Path; // 例如: /api/users + var queryString = request.QueryString; // 例如: ?id=123 + var fullPath = $"{path}{queryString}"; + + // 2. 读取请求方法 + var method = request.Method; // GET, POST, PUT, etc. + + // 3. 读取请求头 + var headers = request.Headers; + + // 4. 读取 Body (重要:需要启用缓冲) + request.EnableBuffering(); - return Task.CompletedTask; + // 如果 Body 已经被其他中间件读取过,需要先重置: + request.Body.Position = 0; + + string body; + using (var reader = new StreamReader( + request.Body, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + request.Body.Position = 0; // 重置流位置,以便后续中间件可以读取 + } + + if (context.Exception is BusinessException) + { + LogHelper.Warn($@" + |-请求路径:{method} {fullPath} + |-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff} + |-请求用户:{headers.Authorization} + |-请求参数:{body} + |-业务异常:{context.Exception}", true); + } + else + { + LogHelper.Error($@" + |-请求路径:{method} {fullPath} + |-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff} + |-请求用户:{headers.Authorization} + |-请求参数:{body} + |-异常堆栈:{context.Exception} "); + } } } \ No newline at end of file diff --git a/src/InterfaceForward.Application/Filters/ForwardExceptionFilter.cs b/src/InterfaceForward.Application/Filters/ForwardExceptionFilter.cs index 5e3adcf..b5d10d5 100644 --- a/src/InterfaceForward.Application/Filters/ForwardExceptionFilter.cs +++ b/src/InterfaceForward.Application/Filters/ForwardExceptionFilter.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations; using InterfaceForward.Application.Contracts; using InterfaceForward.Application.Contracts.ForwardCore; -using InterfaceForward.Application.Rabbit; using InterfaceForward.Repositories.Log.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,8 +12,7 @@ namespace InterfaceForward.Application.Filters; public class ForwardExceptionFilter( ILogRepository logRepository, - IOptions options, - RabbitClient rabbitClient) + IOptions options) : IAsyncExceptionFilter { private readonly InterfaceRelayOptions _options = options.Value; diff --git a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs index 600198d..8b14ddc 100644 --- a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs +++ b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs @@ -61,7 +61,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency public virtual async Task OutParamRestructure(ForwardCoreContext context, JToken response) { // 映射 - var mappedRes = JsonHelper.Restructure(response, context.OutParamTreeList); + var mappedRes = JsonHelper.Restructure(response, context.OutParamTreeList, context.MappedParamType); // 非结构化 if (context.TargetInterface.ResponseContentType is ResponseContentType.csv or ResponseContentType.pdf or ResponseContentType.Base64) diff --git a/src/InterfaceForward.Application/Services/App/AppsService.cs b/src/InterfaceForward.Application/Services/App/AppsService.cs index 4876914..18865b1 100644 --- a/src/InterfaceForward.Application/Services/App/AppsService.cs +++ b/src/InterfaceForward.Application/Services/App/AppsService.cs @@ -94,7 +94,7 @@ public class AppsService : ApplicationService var redisKey = nameof(GetKeyAndSecret) + "_" + date; var num = await RedisHelper.Client.IncrByAsync(redisKey, 1); if (num > 999) throw new BusinessException("超过每月设计的数量"); - var key = "BSI" + date + num.ToString().PadLeft(3, '0'); + var key = "W" + date + num.ToString().PadLeft(3, '0'); //创建一个StringBuilder对象存储密码 StringBuilder secret = new StringBuilder(); diff --git a/src/InterfaceForward.Application/Services/Customized/Shopee.cs b/src/InterfaceForward.Application/Services/Customized/Shopee.cs new file mode 100644 index 0000000..3fdfbd9 --- /dev/null +++ b/src/InterfaceForward.Application/Services/Customized/Shopee.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace InterfaceForward.Application.Services.Customized; + +[ApiExplorerSettings(GroupName = "接口转发服务")] +[TypeFilter(typeof(ShopeeForwardAuthorizeFilter))] +[TypeFilter(typeof(ShopeeForwardExceptionFilter))] +public class Shopee(InterfaceForwardCommon forwardCommon): ApplicationService +{ + /// + /// 动态路由:上游接口code/服务商code/定制化后缀 + /// + /// + /// + /// + /// + /// + // "services/ilh_shipment/create" + [HttpPost("{upStreamCode}/{serviceProviderCode}/{postfix}")] + public async Task ForwardWithSubscribeAsync( + [FromRoute] [Required] string upStreamCode, + [FromRoute] [Required] string serviceProviderCode, + [FromBody] [Required] JToken data) + { + var contextList = await forwardCommon.VerifyBusinessAndBuildForwardContexts(upStreamCode, serviceProviderCode); + + var context = contextList.First(); + context.OriginalInterfaceInput = contextList.Count == 1 + ? data + : new JObject + { + [upStreamCode] = data + }; + + var res = contextList.Count == 1 + ? await forwardCommon.InternalForwardAsync(context) + : await forwardCommon.InternalForward2Async(contextList); + + return res; + } +} \ No newline at end of file diff --git a/src/InterfaceForward.Application/Services/Customized/ShopeeForwardAuthorizeFilter.cs b/src/InterfaceForward.Application/Services/Customized/ShopeeForwardAuthorizeFilter.cs new file mode 100644 index 0000000..634172e --- /dev/null +++ b/src/InterfaceForward.Application/Services/Customized/ShopeeForwardAuthorizeFilter.cs @@ -0,0 +1,87 @@ +using System.Text; +using Fake.AspNetCore.Http; +using InterfaceForward.Repositories; +using InterfaceForward.Repositories.Interface.Entitys; +using InterfaceForward.Repositories.Log.Services; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Newtonsoft.Json; + +namespace InterfaceForward.Application.Services.Customized; + +public class ShopeeForwardAuthorizeFilter( + InterfaceForwardQuery interfaceForwardQuery, + ILogRepository logRepository, + IHttpClientInfoProvider httpClientInfoProvider, + IBasicRepository interfaceRepository) + : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var start = DateTime.Now; + var log = logRepository.AppRequestLog; + log.RequestTime = start; + + var httpContext = context.HttpContext; + + log.IsApp = true; + + log.ClientIP = httpClientInfoProvider.ClientIpAddress; + log.Address = httpContext.Request.GetDisplayUrl(); + + // body + //重置RequestBody的Position为0 + context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(context.HttpContext.Request.Body, Encoding.UTF8)) + { + log.Content = await reader.ReadToEndAsync(); + } + + var headers = httpContext.Request.Headers; + log.Headers = JsonConvert.SerializeObject(headers); + + // string? appKey = headers["appKey"], appSecret = headers["appSecret"]; + // if (appKey.IsNullOrWhiteSpace() || appSecret.IsNullOrWhiteSpace()) + // { + // throw new BusinessException(message: "应用授权失败,AppKey和AppSecret是必填的"); + // } + // + // var app = await interfaceForwardQuery.FindAppAsync(appKey!, appSecret!); + // if (app == null) + // throw new BusinessException(message: "应用授权失败,请检查AppKey或AppSecret是否正确"); + + // 先写死的 + var app = await interfaceForwardQuery.FindAppAsync("W2507080", "G1rA80mAfGovH7j53t5bIH425M4vKreT"); + log.Name = app.Name; + if (!app.IsOnline) + throw new BusinessException(message: "应用已被禁用,请检查应用状态"); + + logRepository.AppId = app.Id; + + var upStreamCode = httpContext.Request.Path.ToUriComponent().Split('/')[1]; + var systemInterface = await interfaceRepository.GetFirstAsync(x => x.Code == upStreamCode, + x => new { x.Code, x.Name }); + if (systemInterface == null) throw new BusinessException(message: "系统接口不存在"); + logRepository.AppRequestLog.InterfaceCode = systemInterface.Code; + logRepository.AppRequestLog.InterfaceName = systemInterface.Name; + + var actionContext = await next(); + + if (actionContext.Exception == null) + { + log.Cost = (int)(DateTime.Now - start).TotalMilliseconds; + + if (actionContext.Result is ObjectResult objectResult) + { + log.Response = JsonConvert.SerializeObject(objectResult.Value); + } + else + { + log.Response = actionContext.Result?.ToString()?? string.Empty; + } + + _ = logRepository.WriteAppLog(); + } + } +} \ No newline at end of file diff --git a/src/InterfaceForward.Application/Services/Customized/ShopeeForwardExceptionFilter.cs b/src/InterfaceForward.Application/Services/Customized/ShopeeForwardExceptionFilter.cs new file mode 100644 index 0000000..1a544f7 --- /dev/null +++ b/src/InterfaceForward.Application/Services/Customized/ShopeeForwardExceptionFilter.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using InterfaceForward.Application.Contracts; +using InterfaceForward.Application.Contracts.ForwardCore; +using InterfaceForward.Repositories.Log.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace InterfaceForward.Application.Services.Customized; + +public class ShopeeForwardExceptionFilter( + ILogRepository logRepository, + IOptions options) + : IAsyncExceptionFilter +{ + private readonly InterfaceRelayOptions _options = options.Value; + + public Task OnExceptionAsync(ExceptionContext context) + { + if (context.ExceptionHandled) return Task.CompletedTask; + + /* + * ServiceProviderRequestLog设计的意义是: + * 最大可能限度的保留请求服务商发生异常时附近的日志信息,以便于排查问题 + */ + var originMessage = bool.TryParse(context.HttpContext.Request.Headers["isReturnMessage"].ToString(), + out var isReturnMessage) + ? isReturnMessage ? logRepository.ServiceProviderRequestLog.Response : null + : null; + + var res = new InterfaceRelayUnifyResultDto + { + Code = StatusCodes.Status400BadRequest.ToString(), + RequestId = logRepository.AppRequestLog.RequestId, + OriginalMessage = originMessage, + Attach = context.HttpContext.Request.Headers["attach"] + }; + + if (context.Exception is ValidationException errors) //验证异常 + { + res.Msg = errors.Message; + context.Result = new JsonResult(res); + } + else if (context.Exception is BusinessException business) //业务异常 + { + res.Msg = business.Message; + context.Result = new JsonResult(res); + } + else //未处理异常 + { + logRepository.SendFeiShu(context.Exception); + res.Code = StatusCodes.Status500InternalServerError.ToString(); + res.Msg = "服务器发生未处理异常"; + context.Result = new JsonResult(res); + } + + if (NeedPush(context, out var subscribeName)) + { + // todo:要改成http回调的方式? + } + + // 异常情况也要记录app日志 + logRepository.AppRequestLog.Response = JsonConvert.SerializeObject(res); + _ = logRepository.WriteAppLog(context.Exception); + context.ExceptionHandled = true; + return Task.CompletedTask; + } + + private bool NeedPush(ExceptionContext context, out string subscribeName) + { + subscribeName = null; + string interfaceCode = context.HttpContext.Request.Query["upStreamCode"]; + subscribeName = context.HttpContext.Request.Headers["failedSubscribeName"]; + + return _options.FailedNeedPush.Contains(interfaceCode) && !subscribeName.IsNullOrWhiteSpace(); + } +} \ No newline at end of file diff --git a/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs b/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs index 001e440..8e62980 100644 --- a/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs +++ b/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs @@ -107,7 +107,8 @@ public class InterfaceForwardCommon( FormFieldList = item.FormFieldList, InParamTreeList = item.InParamTreeList, PrefixScript = item.PrefixScript, - PostfixScript = item.PostfixScript + PostfixScript = item.PostfixScript, + MappedParamType = cacheContext.OriginalInterface.MappedParamType }; var token = context.FixedFieldList.FirstOrDefault(x => x.FieldValue == GlobalConst.Token); if (token != default) diff --git a/src/InterfaceForward.Application/Services/InterfaceForwardService.cs b/src/InterfaceForward.Application/Services/InterfaceForwardService.cs index 3416db3..efc99b7 100644 --- a/src/InterfaceForward.Application/Services/InterfaceForwardService.cs +++ b/src/InterfaceForward.Application/Services/InterfaceForwardService.cs @@ -20,7 +20,6 @@ namespace InterfaceForward.Application.Services; [TypeFilter(typeof(ForwardAuthorizeFilter))] [TypeFilter(typeof(ForwardExceptionFilter))] [AllowAnonymous] -[Route("InterfaceForward")] public class InterfaceForwardService : ApplicationService, IInterfaceForwardService { private readonly InterfaceForwardCommon _forwardCommon; @@ -38,7 +37,7 @@ public class InterfaceForwardService : ApplicationService, IInterfaceForwardServ _httpContextAccessor = httpContextAccessor; } - [HttpPost("BatchForwardThenPushToQueue")] + [HttpPost("InterfaceForward/BatchForwardThenPushToQueue")] public async Task BatchForwardThenPushToQueueAsync( [FromQuery] [Required] string upStreamCode, [FromQuery] [Required] string serviceProviderCode, @@ -111,7 +110,7 @@ public class InterfaceForwardService : ApplicationService, IInterfaceForwardServ }; } - [HttpPost("ForwardWithSubscribe")] + [HttpPost("InterfaceForward/ForwardWithSubscribe")] public async Task ForwardWithSubscribeAsync( [FromQuery] [Required] string upStreamCode, [FromQuery] [Required] string serviceProviderCode, @@ -147,7 +146,7 @@ public class InterfaceForwardService : ApplicationService, IInterfaceForwardServ }; } - [HttpPost("ReForward")] + [HttpPost("InterfaceForward/ReForward")] public async Task ReForwardAsync( [FromQuery] [Required] string upStreamCode, [FromQuery] [Required] string serviceProviderCode, diff --git a/src/InterfaceForward.Application/Services/Parameter/FixedParameterService.cs b/src/InterfaceForward.Application/Services/Parameter/FixedParameterService.cs index bec5863..6699ff4 100644 --- a/src/InterfaceForward.Application/Services/Parameter/FixedParameterService.cs +++ b/src/InterfaceForward.Application/Services/Parameter/FixedParameterService.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Fake.Auditing; using Fake.UnitOfWork; using InterfaceForward.Application.Contracts.Dtos.ServiceProvider; using InterfaceForward.Application.Helpers; @@ -14,7 +15,7 @@ namespace InterfaceForward.Application.Services.Parameter; /// 固定参数配置 /// [ApiExplorerSettings(GroupName = "参数服务")] -public class FixedParameterService : ApplicationService +public class FixedParameterService : ApplicationService, IAuditingEnabled { private readonly IFixedParameterRepository _fixedParameterRepository; private readonly IServiceProviderRepository _serviceProviderRepository; @@ -65,7 +66,7 @@ public class FixedParameterService : ApplicationService /// /// [HttpPost("FixedParameter/MaintainParameter")] - public async Task MaintainParameterAsync([FromBody] FixedParameterInput input) + public virtual async Task MaintainParameterAsync([FromBody] FixedParameterInput input) { // 校验服务商是否存在 if (!await _serviceProviderRepository.IsAnyAsync(x => x.Id == input.ServiceProviderId))