1. 支持系统接口返回参数类型控制
2. 优化异常日志
3. 支持shopee定制化
4. 支持动态路由
This commit is contained in:
xiaolipro 2025-10-30 17:20:08 +08:00
parent fe2b26c76c
commit 5ac381f4f1
14 changed files with 291 additions and 18 deletions

View File

@ -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;

View File

@ -14,7 +14,10 @@
],
"WriteTo": [
{
"Name": "Console"
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"
}
}
]
},

View File

@ -8,7 +8,7 @@ namespace InterfaceForward.Application.Contracts.Dtos.Interface;
public class ParameterParseInput
{
/// <summary>
/// 参数解析类型(0json1xml
/// 参数解析类型(1json2xml
/// </summary>
public ParameterParseTypeEnum ParameterParseType { get; set; }

View File

@ -50,6 +50,11 @@ public class ForwardCoreContext
/// 请求后置脚本
/// </summary>
public string? PostfixScript { get; set; }
/// <summary>
/// 返回参数 映射后参数类型默认Object
/// </summary>
public ParameterType MappedParamType { get; set; }
public bool AddHeader(string key, string value)
{

View File

@ -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<IHttpContextAccessor>().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} ");
}
}
}

View File

@ -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<InterfaceRelayOptions> options,
RabbitClient rabbitClient)
IOptions<InterfaceRelayOptions> options)
: IAsyncExceptionFilter
{
private readonly InterfaceRelayOptions _options = options.Value;

View File

@ -61,7 +61,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
public virtual async Task<JToken> 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)

View File

@ -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();

View File

@ -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
{
/// <summary>
/// 动态路由上游接口code/服务商code/定制化后缀
/// </summary>
/// <param name="upStreamCode"></param>
/// <param name="serviceProviderCode"></param>
/// <param name="data"></param>
/// <returns></returns>
/// <exception cref="BusinessException"></exception>
// "services/ilh_shipment/create"
[HttpPost("{upStreamCode}/{serviceProviderCode}/{postfix}")]
public async Task<JToken> 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;
}
}

View File

@ -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<InterfaceEntity> 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();
}
}
}

View File

@ -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<InterfaceRelayOptions> 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();
}
}

View File

@ -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)

View File

@ -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<InterfaceRelayUnifyResultDto> 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<InterfaceRelayUnifyResultDto> 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<InterfaceRelayUnifyResultDto> ReForwardAsync(
[FromQuery] [Required] string upStreamCode,
[FromQuery] [Required] string serviceProviderCode,

View File

@ -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;
/// 固定参数配置
/// </summary>
[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
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("FixedParameter/MaintainParameter")]
public async Task<bool> MaintainParameterAsync([FromBody] FixedParameterInput input)
public virtual async Task<bool> MaintainParameterAsync([FromBody] FixedParameterInput input)
{
// 校验服务商是否存在
if (!await _serviceProviderRepository.IsAnyAsync(x => x.Id == input.ServiceProviderId))