444 lines
19 KiB
C#
444 lines
19 KiB
C#
using System.Diagnostics;
|
||
using System.Runtime.ExceptionServices;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using Fake.DependencyInjection;
|
||
using InterfaceForward.Application.Contracts;
|
||
using InterfaceForward.Application.Contracts.ForwardCore;
|
||
using InterfaceForward.Application.ForwardCore;
|
||
using InterfaceForward.Application.Helpers;
|
||
using InterfaceForward.Domain.Shared;
|
||
using InterfaceForward.Domain.Shared.Dtos;
|
||
using InterfaceForward.Domain.Shared.Enum;
|
||
using InterfaceForward.Repositories;
|
||
using InterfaceForward.Repositories.Log.Services;
|
||
using InterfaceForward.Repositories.Log.ValueObjects;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Options;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
namespace InterfaceForward.Application.Services;
|
||
|
||
public class InterfaceForwardCommon(
|
||
InterfaceForwardQuery interfaceForwardQuery,
|
||
ILogRepository logRepository,
|
||
IServiceProvider serviceProvider,
|
||
IOptionsSnapshot<InterfaceRelayOptions> options)
|
||
: ITransientDependency
|
||
{
|
||
/// <summary>
|
||
/// 缓存数据:映射,账户 默认过期时间
|
||
/// </summary>
|
||
public static readonly int DefaultCacheTtl = int.MaxValue;
|
||
|
||
private readonly Dictionary<string, IForwardFlow> _flows = new();
|
||
|
||
private readonly InterfaceRelayOptions _options = options.Value;
|
||
|
||
public async Task<List<ForwardCoreContext>> VerifyBusinessAndBuildForwardContexts(string upStreamCode,
|
||
string serviceProviderCode)
|
||
{
|
||
// todo:字符串优化
|
||
// tips:上下文的缓存不维护,在发布时统一替代。
|
||
var contextCacheKey = GetContextCacheKey(upStreamCode, serviceProviderCode);
|
||
var cacheContext = (await RedisHelper.Client.GetAsync<string>(contextCacheKey))
|
||
.ToObject<ForwardCoreContextCache>();
|
||
if (cacheContext == null)
|
||
{
|
||
var systemInterface = await interfaceForwardQuery.GetInterfaceByCodesAsync(upStreamCode);
|
||
if (systemInterface == null) throw new BusinessException(message: "系统接口不存在");
|
||
|
||
logRepository.AppRequestLog.InterfaceCode = systemInterface.Code;
|
||
logRepository.AppRequestLog.InterfaceName = systemInterface.Name;
|
||
|
||
var serviceProviderDto = await interfaceForwardQuery.GetServiceProviderByCodeAsync(serviceProviderCode);
|
||
if (serviceProviderDto == null) throw new BusinessException(message: "服务商不存在");
|
||
cacheContext = await interfaceForwardQuery.BuildForwardCoreContext(systemInterface, serviceProviderDto);
|
||
await RedisHelper.Client.SetAsync(contextCacheKey, cacheContext.ToJson(), DefaultCacheTtl);
|
||
}
|
||
|
||
// 记录系统接口日志
|
||
logRepository.AppRequestLog.InterfaceCode = cacheContext.OriginalInterface.Code;
|
||
logRepository.AppRequestLog.InterfaceName = cacheContext.OriginalInterface.Name;
|
||
|
||
// tips:服务商主体账户的缓存实时维护
|
||
var accountCacheKey = GetAccountCacheKey(serviceProviderCode, logRepository.AppId);
|
||
var accountFieldCache = (await RedisHelper.Client.GetAsync<string>(accountCacheKey))
|
||
.ToObject<List<ServiceProviderAccountFieldWithValueDto>>();
|
||
if (accountFieldCache == null)
|
||
{
|
||
var serviceProviderDto = cacheContext.TargetServiceProvider;
|
||
var accountId = await interfaceForwardQuery.GetAccountIdAsync(serviceProviderDto.Id, logRepository.AppId);
|
||
// 服务商授权
|
||
if (!serviceProviderDto.IsDisableAuth && accountId == default)
|
||
{
|
||
throw new BusinessException(message: $"服务商:{serviceProviderDto.Name} 下不存在此账户,请联系管理员或前往接口通创建");
|
||
}
|
||
|
||
accountFieldCache = await interfaceForwardQuery.GetAccountFieldListAsync(accountId);
|
||
accountFieldCache.Add(new ServiceProviderAccountFieldWithValueDto
|
||
{
|
||
FieldName = GlobalConst.AccountHashCode,
|
||
// 根据字段值计算hash,请求时作为qps key依据,即相同的账户会共享一个qps
|
||
FieldValue = GetHash(accountFieldCache.Select(x => x.FieldValue).JoinAsString("_"))
|
||
});
|
||
var ttl = await HandleAuthInterfaceAsync(cacheContext, accountFieldCache);
|
||
|
||
await RedisHelper.Client.SetAsync(accountCacheKey, accountFieldCache.ToJson(), ttl);
|
||
}
|
||
|
||
// 组装转发上下文
|
||
var res = new List<ForwardCoreContext>();
|
||
|
||
foreach (var item in cacheContext.TargetInterfaces)
|
||
{
|
||
item.TargetInterface.Timeout ??= cacheContext.TargetServiceProvider.Timeout;
|
||
item.TargetInterface.FlowCode = item.TargetInterface.FlowCode.IsNullOrWhiteSpace()
|
||
? cacheContext.TargetServiceProvider.FlowCode.IsNullOrWhiteSpace()
|
||
? nameof(DefaultForwardFlow)
|
||
: cacheContext.TargetServiceProvider.FlowCode
|
||
: item.TargetInterface.FlowCode;
|
||
var context = new ForwardCoreContext
|
||
{
|
||
TargetInterface = item.TargetInterface,
|
||
FixedFieldList = item.FixedFieldList,
|
||
AccountFieldList = accountFieldCache,
|
||
ReturnConfigList = item.ReturnConfigList,
|
||
FormFieldList = item.FormFieldList,
|
||
InParamTreeList = item.InParamTreeList,
|
||
PrefixScript = item.PrefixScript,
|
||
PostfixScript = item.PostfixScript
|
||
};
|
||
var token = context.FixedFieldList.FirstOrDefault(x => x.FieldValue == GlobalConst.Token);
|
||
if (token != default)
|
||
{
|
||
token.FieldValue = context.GetAccountFieldValueOrNull(GlobalConst.Token)!;
|
||
}
|
||
|
||
context.Feature.AccountHashCode = context.GetAccountFieldValueOrNull(GlobalConst.AccountHashCode)!;
|
||
|
||
context.ServiceProviderRequestLog = new RequestLogDto(logRepository.AppRequestLog.RequestId);
|
||
context.ServiceProviderRequestLog.Name = cacheContext.TargetServiceProvider.Name;
|
||
context.ServiceProviderRequestLog.Address = context.TargetInterface.RequestAddress;
|
||
context.ServiceProviderRequestLog.InterfaceCode = context.TargetInterface.Code;
|
||
context.ServiceProviderRequestLog.InterfaceName = context.TargetInterface.Name;
|
||
|
||
Debug.Assert(context.TargetInterface.FlowCode != null, "context.TargetInterface.FlowCode != null");
|
||
var flow = GetForwardFlow(context.TargetInterface.FlowCode);
|
||
if (flow == default)
|
||
throw new BusinessException(message: "找不到接口转发流程" + context.TargetInterface.FlowCode);
|
||
_flows.Add(context.TargetInterface.Code, flow);
|
||
|
||
res.Add(context);
|
||
}
|
||
|
||
// tips:把出参映射树放到最后一个上下文中,是为了在流程结束时执行出参映射
|
||
res.Last().OutParamTreeList = cacheContext.OutParamTreeList;
|
||
|
||
return res;
|
||
}
|
||
|
||
// 优先用服务商接口指定流程,其次使用服务商的,再其次是默认的
|
||
public IForwardFlow GetForwardFlow(string flowCode)
|
||
{
|
||
return serviceProvider.GetServices<IForwardFlow>()
|
||
.FirstOrDefault(x => x.GetType().Name == flowCode)?? throw new BusinessException("");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理服务商授权接口,返回一个过期时间表示服务商账户缓存的有效期
|
||
/// tips:因为我们将授权接口的返回值作为服务商账户字段的一部分一起缓存,所以这里需要返回过期时间
|
||
/// </summary>
|
||
/// <param name="cacheContext"></param>
|
||
/// <param name="accountFieldCache"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||
public virtual async Task<int> HandleAuthInterfaceAsync(ForwardCoreContextCache cacheContext,
|
||
List<ServiceProviderAccountFieldWithValueDto> accountFieldCache)
|
||
{
|
||
var interfaceDto = cacheContext.ServiceProviderAuthInterface;
|
||
|
||
if (interfaceDto == default)
|
||
{
|
||
return DefaultCacheTtl;
|
||
}
|
||
|
||
var configs = cacheContext.ServiceProviderAuthConfigs;
|
||
|
||
// 构建入参
|
||
var body = new JObject();
|
||
foreach (var item in configs.Where(x => x.Flag == AuthParameterFlag.InParameter))
|
||
{
|
||
Debug.Assert(item.ParameterAlias != null, "item.ParameterAlias != null");
|
||
body[item.ParameterAlias] = item.Value1;
|
||
}
|
||
|
||
// 构建授权上下文
|
||
interfaceDto.TargetInterface.FlowCode = interfaceDto.TargetInterface.FlowCode.IsNullOrWhiteSpace()
|
||
? cacheContext.TargetServiceProvider.FlowCode.IsNullOrWhiteSpace()
|
||
? nameof(DefaultForwardFlow)
|
||
: cacheContext.TargetServiceProvider.FlowCode
|
||
: interfaceDto.TargetInterface.FlowCode;
|
||
var authContext = new ForwardCoreContext
|
||
{
|
||
TargetInterface = interfaceDto.TargetInterface,
|
||
FixedFieldList = interfaceDto.FixedFieldList, // can filter service provider fix fields
|
||
AccountFieldList = accountFieldCache, // can add from cache
|
||
ReturnConfigList = interfaceDto.ReturnConfigList,
|
||
InParamTreeList = null,
|
||
OutParamTreeList = null,
|
||
PrefixScript = interfaceDto.PrefixScript,
|
||
PostfixScript = interfaceDto.PostfixScript,
|
||
OriginalInterfaceInput = body,
|
||
FormFieldList = interfaceDto.FormFieldList // normal case is null
|
||
};
|
||
authContext.ServiceProviderRequestLog = new RequestLogDto(logRepository.AppRequestLog.RequestId)
|
||
{
|
||
Name = cacheContext.TargetServiceProvider.Name,
|
||
Address = authContext.TargetInterface.RequestAddress,
|
||
InterfaceCode = authContext.TargetInterface.Code,
|
||
InterfaceName = authContext.TargetInterface.Name
|
||
};
|
||
|
||
// 授权也走主流程
|
||
if (!cacheContext.TargetServiceProvider.AuthInterfaceCode.IsNullOrWhiteSpace())
|
||
{
|
||
Debug.Assert(cacheContext.ServiceProviderAuthInterface != null, "cacheContext.ServiceProviderAuthInterface != null");
|
||
var flow = GetForwardFlow(cacheContext.ServiceProviderAuthInterface.TargetInterface.FlowCode!);
|
||
if (flow == default)
|
||
throw new BusinessException(message: "找不到接口转发流程" +
|
||
cacheContext.ServiceProviderAuthInterface.TargetInterface
|
||
.FlowCode);
|
||
_flows.Add(cacheContext.ServiceProviderAuthInterface.TargetInterface.Code, flow);
|
||
}
|
||
|
||
// 请求授权接口
|
||
var response = await InternalForwardAsync(authContext);
|
||
|
||
// 添加到固定字段以便标准转发流程使用
|
||
var tokenConfig = configs.FirstOrDefault(x => x.Flag == AuthParameterFlag.Token);
|
||
if (tokenConfig != default)
|
||
{
|
||
Debug.Assert(tokenConfig.ParameterAlias != null, "tokenConfig.ParameterAlias != null");
|
||
accountFieldCache.Add(new ServiceProviderAccountFieldWithValueDto
|
||
{
|
||
FieldName = GlobalConst.Token,
|
||
FieldValue = tokenConfig.Value1.IsNullOrWhiteSpace()
|
||
? response.SelectToken(tokenConfig.ParameterAlias)?.ToString() ??
|
||
throw new BusinessException($"授权接口返回报文中未找到{tokenConfig.ParameterAlias}")
|
||
: tokenConfig.Value1!.Trim() + " " + response.SelectToken(tokenConfig.ParameterAlias)
|
||
});
|
||
}
|
||
|
||
// 计算过期时间
|
||
var fixedEffectTimeConfig = configs.FirstOrDefault(x => x.Flag == AuthParameterFlag.FixedEffectTime);
|
||
int.TryParse(fixedEffectTimeConfig?.Value1, out var ttl);
|
||
var effectTimeConfig = configs.FirstOrDefault(x => x.Flag == AuthParameterFlag.EffectTime);
|
||
if (effectTimeConfig == default)
|
||
{
|
||
return ttl;
|
||
}
|
||
|
||
Debug.Assert(effectTimeConfig.ParameterAlias != null, "effectTimeConfig.ParameterAlias != null");
|
||
if (int.TryParse(response.SelectToken(effectTimeConfig.ParameterAlias)?.ToString(), out var expiredValue))
|
||
{
|
||
Debug.Assert(effectTimeConfig.Value1 != null, "effectTimeConfig.Value1 != null");
|
||
ttl = Enum.Parse<EffectTimeUnit>(effectTimeConfig.Value1) switch
|
||
{
|
||
EffectTimeUnit.Second => expiredValue,
|
||
EffectTimeUnit.Minute => expiredValue * 60,
|
||
EffectTimeUnit.Hour => expiredValue * 60 * 60,
|
||
_ => throw new ArgumentOutOfRangeException()
|
||
};
|
||
}
|
||
|
||
return ttl;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取上下文redis key
|
||
/// </summary>
|
||
/// <param name="upStreamCode">系统接口code</param>
|
||
/// <param name="serviceProviderCode">服务商code</param>
|
||
/// <returns></returns>
|
||
public static string GetContextCacheKey(string upStreamCode, string serviceProviderCode)
|
||
{
|
||
return $"ForwardContext:up_{upStreamCode}:sp_{serviceProviderCode}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取账户redis key
|
||
/// ex:
|
||
/// 如果appId==null,则返回:ForwardAccount:sp_{serviceProviderCode}:app_*
|
||
/// 如果appId!=null,则返回:ForwardAccount:sp_{serviceProviderCode}:app_{appId}
|
||
/// </summary>
|
||
/// <param name="serviceProviderCode">服务商code</param>
|
||
/// <param name="appId">应用id</param>
|
||
/// <returns></returns>
|
||
public static string GetAccountCacheKey(string serviceProviderCode, int? appId = null)
|
||
{
|
||
return $"ForwardAccount:sp_{serviceProviderCode}:app_{(appId == null ? "*" : appId)}";
|
||
}
|
||
|
||
public async Task<object> InternalForward3Async(List<ForwardCoreContext> contextList, List<RequestLogSummary> logs)
|
||
{
|
||
JToken ans = new JObject();
|
||
// 服务商接口X入参={系统入参数+服务商接口x-1返回参数+服务商接口x-2返回参数+..}
|
||
var input = contextList.First().OriginalInterfaceInput;
|
||
var retry = false;
|
||
for (var i = 0; i < contextList.Count; i++)
|
||
{
|
||
JToken res;
|
||
if (retry)
|
||
{
|
||
// 发起真实请求
|
||
res = await InternalForwardAsync(contextList[i], true);
|
||
}
|
||
else
|
||
{
|
||
var log = logs.FirstOrDefault(x => x.InterfaceCode == contextList[i].TargetInterface.Code);
|
||
// 如果是成功的,直接取日志内容
|
||
if (log is { IsSuccess: true })
|
||
{
|
||
res = JToken.Parse(log.Response);
|
||
}
|
||
else
|
||
{
|
||
res = await InternalForwardAsync(contextList[i], true);
|
||
// 1.当第一次真正发起请求,后面的都需要重试了,解决参数依赖问题。
|
||
// 2.流程变更问题,即以当前缓存中的流程为准,而非历史流程。
|
||
retry = true;
|
||
}
|
||
}
|
||
|
||
ans[contextList[i].TargetInterface.Code] = res;
|
||
|
||
if (i + 1 < contextList.Count)
|
||
{
|
||
// 合并请求结果作为下一个请求的入参
|
||
input[contextList[i].TargetInterface.Code] = res;
|
||
contextList[i + 1].OriginalInterfaceInput = input;
|
||
}
|
||
}
|
||
|
||
// 最后做一次反参映射,而不是每次
|
||
var context = contextList.Last();
|
||
return await _flows[context.TargetInterface.Code].OutParamRestructure(context, ans);
|
||
}
|
||
|
||
public async Task<JToken> InternalForward2Async(List<ForwardCoreContext> contextList)
|
||
{
|
||
JToken ans = new JObject();
|
||
var input = contextList.First().OriginalInterfaceInput;
|
||
for (var i = 0; i < contextList.Count; i++)
|
||
{
|
||
var res = await InternalForwardAsync(contextList[i], true);
|
||
ans[contextList[i].TargetInterface.Code] = res;
|
||
if (i + 1 < contextList.Count)
|
||
{
|
||
// 并入返回内容作为下一次请求的入参部分
|
||
input[contextList[i].TargetInterface.Code] = res;
|
||
contextList[i + 1].OriginalInterfaceInput = input;
|
||
}
|
||
}
|
||
|
||
// 最后做一次反参映射,而不是每次
|
||
var context = contextList.Last();
|
||
return await _flows[context.TargetInterface.Code].OutParamRestructure(context, ans);
|
||
}
|
||
|
||
public async Task<JToken> InternalForwardAsync(ForwardCoreContext context, bool isMulti = false)
|
||
{
|
||
try
|
||
{
|
||
context.Feature.TargetInterfaceQps = context.TargetInterface.Qps <= 0
|
||
? _options.ServiceProviderDefaultQps // 默认qps上限
|
||
: context.TargetInterface.Qps;
|
||
context.Feature.SpinPeriod = _options.SpinPeriod; // 异步自旋周期
|
||
|
||
var flow = _flows[context.TargetInterface.Code];
|
||
|
||
context.OriginalInterfaceMappedInput = await flow.InParamRestructure(context);
|
||
|
||
var response = await flow.ForwardAsync(context);
|
||
|
||
if (isMulti) return response; // 一对多不在这映射
|
||
var res = await flow.OutParamRestructure(context, response);
|
||
if (context.Feature.IsWait)
|
||
{
|
||
context.ServiceProviderRequestLog.Response2 = res.ToJson();
|
||
}
|
||
|
||
return res;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
context.ServiceProviderRequestLog.IsSuccess = false;
|
||
context.ServiceProviderRequestLog.Exception = ex.ToString();
|
||
ExceptionDispatchInfo.Capture(ex).Throw();
|
||
|
||
return default;
|
||
}
|
||
finally
|
||
{
|
||
if (context.ServiceProviderRequestLog.RequestTime == default)
|
||
{
|
||
context.ServiceProviderRequestLog.RequestTime = DateTime.Now;
|
||
}
|
||
|
||
// 关联当前服务商日志
|
||
logRepository.ServiceProviderRequestLog = context.ServiceProviderRequestLog;
|
||
// 写服务商日志
|
||
await logRepository.AddLogAsync(context.ServiceProviderRequestLog);
|
||
}
|
||
}
|
||
|
||
public List<JObject> ObjectToObjectArray(object data)
|
||
{
|
||
if (JToken.FromObject(data) is not JArray jarray)
|
||
{
|
||
throw new BusinessException(message: "非法结构,请传对象数组");
|
||
}
|
||
|
||
var objectArray = new List<JObject>(jarray.Count);
|
||
foreach (var item in jarray) //数组对象校验
|
||
{
|
||
if (item is not JObject jobect)
|
||
{
|
||
throw new BusinessException(message: "批量处理必须传递对象数组");
|
||
}
|
||
|
||
objectArray.Add(jobect);
|
||
}
|
||
|
||
if (objectArray.Count < 1)
|
||
{
|
||
throw new BusinessException(message: "请勿传递空数组");
|
||
}
|
||
|
||
return objectArray;
|
||
}
|
||
|
||
/// <summary>获取字符串的hash值</summary>
|
||
/// <param name="str"></param>
|
||
/// <returns></returns>
|
||
public static string GetHash(string str)
|
||
{
|
||
// 将字符串转换为字节
|
||
byte[] data = Encoding.UTF8.GetBytes(str);
|
||
|
||
// 创建SHA256实例
|
||
using SHA256 sha256 = SHA256.Create();
|
||
// 计算哈希值
|
||
byte[] hash = sha256.ComputeHash(data);
|
||
|
||
// 将哈希值转换为16进制字符串
|
||
StringBuilder sb = new StringBuilder();
|
||
foreach (byte b in hash)
|
||
{
|
||
sb.Append(b.ToString("x2"));
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
} |