feat:重构

This commit is contained in:
xiaolipro 2026-06-03 14:49:58 +08:00
parent 432ea493d0
commit d70c9545a6
21 changed files with 756 additions and 64 deletions

View File

@ -42,6 +42,11 @@ namespace InterfaceForward.Application.Contracts.Dtos.Interface
///</summary> ///</summary>
public ParameterType Type { get; set; } public ParameterType Type { get; set; }
/// <summary>
/// 参数用途
/// </summary>
public ParameterParaKind ParaKind { get; set; }
/// <summary> /// <summary>
/// 是否必填 /// 是否必填
/// </summary> /// </summary>

View File

@ -106,17 +106,37 @@ public class SaveInterfaceInput
public ParameterType? MappedParamType { get; set; } public ParameterType? MappedParamType { get; set; }
/// <summary> /// <summary>
/// 入参列表 /// 上游入参列表
/// </summary> /// </summary>
public List<ParameterInput> InParameterList { get; set; } = new(); public List<ParameterInput> InParameterList { get; set; } = new();
/// <summary>
/// Json/Xml 请求体参数树
/// </summary>
public List<ParameterInput> RequestBodyParameterList { get; set; } = new();
/// <summary>
/// Form 类映射体参数树
/// </summary>
public List<ParameterInput> MappingBodyParameterList { get; set; } = new();
/// <summary> /// <summary>
/// 出参列表 /// 出参列表
/// </summary> /// </summary>
public List<ParameterInput> OutParameterList { get; set; } = new(); public List<ParameterInput> OutParameterList { get; set; } = new();
/// <summary> /// <summary>
/// 表单列表 /// Query KV 列表
/// </summary> /// </summary>
public List<FormParameterInput> FormParameterList { get; set; } = new(); public List<FormParameterInput> QueryFormParameterList { get; set; } = new();
/// <summary>
/// Header KV 列表
/// </summary>
public List<FormParameterInput> HeaderFormParameterList { get; set; } = new();
/// <summary>
/// Body Form KV 列表
/// </summary>
public List<FormParameterInput> BodyFormParameterList { get; set; } = new();
} }

View File

@ -31,6 +31,16 @@ public class ForwardCoreContext
/// </summary> /// </summary>
public List<InterfaceFormFieldDto> FormFieldList { get; set; } = []; public List<InterfaceFormFieldDto> FormFieldList { get; set; } = [];
/// <summary>
/// Query 字段列表
/// </summary>
public List<InterfaceFormFieldDto> QueryFieldList { get; set; } = [];
/// <summary>
/// Header 字段列表
/// </summary>
public List<InterfaceFormFieldDto> HeaderFieldList { get; set; } = [];
/// <summary> /// <summary>
/// 入参映射列表(树形结构) /// 入参映射列表(树形结构)
/// </summary> /// </summary>

View File

@ -279,6 +279,12 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
// 将参数绑定到query // 将参数绑定到query
var builder = new UriBuilder(targetInterface.RequestAddress); var builder = new UriBuilder(targetInterface.RequestAddress);
var query = HttpUtility.ParseQueryString(builder.Query); var query = HttpUtility.ParseQueryString(builder.Query);
foreach (var item in context.QueryFieldList)
{
query.Remove(item.Name);
query.Add(item.Name, item.Value);
}
foreach (var item in context.FixedFieldList.Where(x => x.FieldPosition == FieldPosition.Query)) foreach (var item in context.FixedFieldList.Where(x => x.FieldPosition == FieldPosition.Query))
{ {
query.Remove(item.FieldName); query.Remove(item.FieldName);
@ -293,6 +299,16 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
SetContentType(context, msg); SetContentType(context, msg);
// 将参数绑定到header // 将参数绑定到header
foreach (var item in context.HeaderFieldList)
{
if (msg.Headers.Contains(item.Name))
msg.Headers.Remove(item.Name);
if (!msg.Headers.TryAddWithoutValidation(item.Name, item.Value))
{
ThrowBusinessException(context, $"请求头无法插入 {item.Name}{item.Value}");
}
}
foreach (var item in context.FixedFieldList.Where(x => x.FieldPosition == FieldPosition.Header)) foreach (var item in context.FixedFieldList.Where(x => x.FieldPosition == FieldPosition.Header))
{ {
//msg.Content.Headers.TryAddWithoutValidation(item.FieldName, item.FieldValue); //msg.Content.Headers.TryAddWithoutValidation(item.FieldName, item.FieldValue);
@ -399,7 +415,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
Dictionary<string, string> nameValueCollection; Dictionary<string, string> nameValueCollection;
if (context.FormFieldList.Count != 0) if (context.FormFieldList.Count != 0)
{ {
var field = context.FormFieldList.FirstOrDefault(x => x.Value == GlobalConst.FromBody); var field = context.FormFieldList.FirstOrDefault(x => x.Value == GlobalConst.FromMappingBody);
if (field != null) field.Value = body; if (field != null) field.Value = body;
nameValueCollection = context.FormFieldList.ToDictionary(x => x.Name, x => x.Value); nameValueCollection = context.FormFieldList.ToDictionary(x => x.Name, x => x.Value);
} }
@ -504,19 +520,20 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
} }
} }
// 表单 // Query / Header / 表单
foreach (var item in context.QueryFieldList)
{
item.Value = ReplaceFormPlaceholder(item.Name, item.Value, item.Description);
}
foreach (var item in context.HeaderFieldList)
{
item.Value = ReplaceFormPlaceholder(item.Name, item.Value, item.Description);
}
foreach (var item in context.FormFieldList) foreach (var item in context.FormFieldList)
{ {
item.Value = item.Value switch item.Value = ReplaceFormPlaceholder(item.Name, item.Value, item.Description);
{
GlobalConst.TimeStamp => item.Description == "ms"
? timeStamp.ToString()
: (timeStamp / 1000).ToString(), // s
GlobalConst.DateTime => TurnDate(now, item.Description),
GlobalConst.Url => context.TargetInterface.RequestAddress,
GlobalConst.FromAccount => context.GetAccountFieldValueOrNull(item.Name) ?? string.Empty,
_ => item.Value // 不处理
};
} }
// 绑定body部分的字段 // 绑定body部分的字段
@ -551,6 +568,21 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
return dateTime.ToString(GlobalConst.DefaultDateStyle); return dateTime.ToString(GlobalConst.DefaultDateStyle);
} }
} }
string ReplaceFormPlaceholder(string name, string value, string? description)
{
return value switch
{
GlobalConst.TimeStamp => description == "ms"
? timeStamp.ToString()
: (timeStamp / 1000).ToString(),
GlobalConst.DateTime => TurnDate(now, description),
GlobalConst.Url => context.TargetInterface.RequestAddress,
GlobalConst.FromAccount => context.GetAccountFieldValueOrNull(name) ?? string.Empty,
GlobalConst.FromMappingBody => GlobalConst.FromMappingBody,
_ => value
};
}
} }
void ReplaceCore(JToken obj, string path, JToken value) void ReplaceCore(JToken obj, string path, JToken value)

View File

@ -109,12 +109,22 @@ public class InterfaceService : ApplicationService
// 构造参数 // 构造参数
var paras = await InterfaceRepository.GetParameterListByIdAsync(id); var paras = await InterfaceRepository.GetParameterListByIdAsync(id);
// 入参树形列表 // 入参树形列表
res.InParameterList = paras.Where(x => x.IsInPara).ToList(); res.InParameterList = interfaceEntity.IsUpStream
? paras.Where(x => x.IsInPara && x.ParaKind == ParameterParaKind.Normal).ToList()
: [];
res.RequestBodyParameterList = !interfaceEntity.IsUpStream && interfaceEntity.ContentType is ContentType.Json or ContentType.Xml
? paras.Where(x => x.IsInPara && x.ParaKind == ParameterParaKind.Normal).ToList()
: [];
res.MappingBodyParameterList = !interfaceEntity.IsUpStream && interfaceEntity.ContentType == ContentType.FormUrlEncoded
? paras.Where(x => x.IsInPara && x.ParaKind == ParameterParaKind.MappingBody).ToList()
: [];
// 出参树形列表 // 出参树形列表
res.OutParameterList = paras.Where(x => !x.IsInPara).ToList(); res.OutParameterList = paras.Where(x => !x.IsInPara).ToList();
// 表单列表 // 表单列表
res.FormParameterList = await FormParameterService.GetParameterListAsync(id); res.QueryFormParameterList = await FormParameterService.GetParameterListAsync(id, FieldPosition.Query);
res.HeaderFormParameterList = await FormParameterService.GetParameterListAsync(id, FieldPosition.Header);
res.BodyFormParameterList = await FormParameterService.GetParameterListAsync(id, FieldPosition.Body);
return res; return res;
} }
@ -133,16 +143,9 @@ public class InterfaceService : ApplicationService
[HttpPost("Interface/SaveInterface")] [HttpPost("Interface/SaveInterface")]
public async Task<int> SaveInterfaceAsync(SaveInterfaceInput input) public async Task<int> SaveInterfaceAsync(SaveInterfaceInput input)
{ {
input.InParameterList.ForEach(x => NormalizeRootArrayName(GetInputParameterList(input));
{ NormalizeRootArrayName(input.OutParameterList);
if (x.PId == 0 && x.Type == ParameterType.Array && x.Name == GlobalConst.RootParameterName) ValidateBodyLists(input);
x.Name = string.Empty;
});
input.OutParameterList.ForEach(x =>
{
if (x.PId == 0 && x.Type == ParameterType.Array && x.Name == GlobalConst.RootParameterName)
x.Name = string.Empty;
});
switch (input.OptionType) switch (input.OptionType)
{ {
@ -209,7 +212,7 @@ public class InterfaceService : ApplicationService
// 校验参数合法性 // 校验参数合法性
ParameterService.ValidateParameter( ParameterService.ValidateParameter(
JsonConvert.DeserializeObject<List<ParameterInput>>( JsonConvert.DeserializeObject<List<ParameterInput>>(
JsonConvert.SerializeObject(input.InParameterList))!); JsonConvert.SerializeObject(GetInputParameterList(input)))!);
ParameterService.ValidateParameter( ParameterService.ValidateParameter(
JsonConvert.DeserializeObject<List<ParameterInput>>( JsonConvert.DeserializeObject<List<ParameterInput>>(
JsonConvert.SerializeObject(input.OutParameterList))!); JsonConvert.SerializeObject(input.OutParameterList))!);
@ -248,11 +251,11 @@ public class InterfaceService : ApplicationService
var interfaceId = await InterfaceRepository.InsertReturnIdentityAsync(interfaceEntity); var interfaceId = await InterfaceRepository.InsertReturnIdentityAsync(interfaceEntity);
// 持久化参数 // 持久化参数
await ParameterService.CreateParametersAsync(input.InParameterList, interfaceId, true); await CreateInputParametersAsync(input, interfaceId);
await ParameterService.CreateParametersAsync(input.OutParameterList, interfaceId, false); await ParameterService.CreateParametersAsync(input.OutParameterList, interfaceId, false);
// 持久化表单参数 // 持久化表单参数
await FormParameterService.CreateParametersAsync(input.FormParameterList, interfaceId); await CreateFormParametersAsync(input, interfaceId);
// 接口代码生成时并发兜底方案 // 接口代码生成时并发兜底方案
if (await InterfaceRepository.CountAsync(x => x.Code.Equals(interfaceEntity.Code)) > 1) if (await InterfaceRepository.CountAsync(x => x.Code.Equals(interfaceEntity.Code)) > 1)
@ -321,14 +324,119 @@ public class InterfaceService : ApplicationService
}); });
// step2、维护参数增删改 // step2、维护参数增删改
await ParameterService.MaintainParametersAsync(input.InParameterList, (int)input.Id, true); await MaintainInputParametersAsync(input, (int)input.Id);
await ParameterService.MaintainParametersAsync(input.OutParameterList, (int)input.Id, false); await ParameterService.MaintainParametersAsync(input.OutParameterList, (int)input.Id, false);
// step3、更新表单参数 // step3、更新表单参数
await FormParameterService.MaintainParametersAsync(input.FormParameterList, interfaceEntity.Id); await MaintainFormParametersAsync(input, interfaceEntity.Id);
// 提交事务 // 提交事务
ts.Complete(); ts.Complete();
} }
private static List<ParameterInput> GetInputParameterList(SaveInterfaceInput input)
{
if (input.IsUpStream) return input.InParameterList;
return input.ContentType switch
{
ContentType.Json or ContentType.Xml => input.RequestBodyParameterList,
ContentType.FormUrlEncoded => input.MappingBodyParameterList,
_ => []
};
}
private static void NormalizeRootArrayName(List<ParameterInput> list)
{
list.ForEach(x =>
{
if (x.PId == 0 && x.Type == ParameterType.Array && x.Name == GlobalConst.RootParameterName)
x.Name = string.Empty;
});
}
private static void ValidateBodyLists(SaveInterfaceInput input)
{
if (input.IsUpStream)
{
if (input.RequestBodyParameterList.Count > 0 || input.MappingBodyParameterList.Count > 0 || input.BodyFormParameterList.Count > 0)
throw new BusinessException(message: "上游接口只能维护 inParameterList");
return;
}
switch (input.ContentType)
{
case ContentType.Json:
case ContentType.Xml:
if (input.InParameterList.Count > 0 || input.MappingBodyParameterList.Count > 0 || input.BodyFormParameterList.Count > 0)
throw new BusinessException(message: "Json/Xml 下游接口只能维护 requestBodyParameterList");
break;
case ContentType.FormUrlEncoded:
if (input.InParameterList.Count > 0 || input.RequestBodyParameterList.Count > 0)
throw new BusinessException(message: "FormUrlEncoded 下游接口只能维护 bodyFormParameterList 和 mappingBodyParameterList");
break;
case ContentType.FormData:
throw new BusinessException(message: "暂时不支持该ContentType");
default:
throw new ArgumentOutOfRangeException();
}
}
private async Task CreateInputParametersAsync(SaveInterfaceInput input, int interfaceId)
{
if (input.IsUpStream)
{
await ParameterService.CreateParametersAsync(input.InParameterList, interfaceId, true);
return;
}
switch (input.ContentType)
{
case ContentType.Json:
case ContentType.Xml:
await ParameterService.CreateParametersAsync(input.RequestBodyParameterList, interfaceId, true);
break;
case ContentType.FormUrlEncoded:
await ParameterService.CreateParametersAsync(input.MappingBodyParameterList, interfaceId, true,
ParameterParaKind.MappingBody);
break;
}
}
private async Task MaintainInputParametersAsync(SaveInterfaceInput input, int interfaceId)
{
if (input.IsUpStream)
{
await ParameterService.MaintainParametersAsync(input.InParameterList, interfaceId, true);
return;
}
switch (input.ContentType)
{
case ContentType.Json:
case ContentType.Xml:
await ParameterService.MaintainParametersAsync(input.RequestBodyParameterList, interfaceId, true);
break;
case ContentType.FormUrlEncoded:
await ParameterService.MaintainParametersAsync(input.MappingBodyParameterList, interfaceId, true,
ParameterParaKind.MappingBody);
break;
}
}
private async Task CreateFormParametersAsync(SaveInterfaceInput input, int interfaceId)
{
await FormParameterService.CreateParametersAsync(input.QueryFormParameterList, interfaceId, FieldPosition.Query);
await FormParameterService.CreateParametersAsync(input.HeaderFormParameterList, interfaceId, FieldPosition.Header);
if (!input.IsUpStream && input.ContentType == ContentType.FormUrlEncoded)
await FormParameterService.CreateParametersAsync(input.BodyFormParameterList, interfaceId, FieldPosition.Body);
}
private async Task MaintainFormParametersAsync(SaveInterfaceInput input, int interfaceId)
{
await FormParameterService.MaintainParametersAsync(input.QueryFormParameterList, interfaceId, FieldPosition.Query);
await FormParameterService.MaintainParametersAsync(input.HeaderFormParameterList, interfaceId, FieldPosition.Header);
if (!input.IsUpStream && input.ContentType == ContentType.FormUrlEncoded)
await FormParameterService.MaintainParametersAsync(input.BodyFormParameterList, interfaceId, FieldPosition.Body);
}
} }

View File

@ -105,6 +105,8 @@ public class InterfaceForwardCommon(
AccountFieldList = accountFieldCache, AccountFieldList = accountFieldCache,
ReturnConfigList = item.ReturnConfigList, ReturnConfigList = item.ReturnConfigList,
FormFieldList = item.FormFieldList, FormFieldList = item.FormFieldList,
QueryFieldList = item.QueryFieldList,
HeaderFieldList = item.HeaderFieldList,
InParamTreeList = item.InParamTreeList, InParamTreeList = item.InParamTreeList,
PrefixScript = item.PrefixScript, PrefixScript = item.PrefixScript,
PostfixScript = item.PostfixScript, PostfixScript = item.PostfixScript,
@ -192,7 +194,9 @@ public class InterfaceForwardCommon(
PrefixScript = interfaceDto.PrefixScript, PrefixScript = interfaceDto.PrefixScript,
PostfixScript = interfaceDto.PostfixScript, PostfixScript = interfaceDto.PostfixScript,
OriginalInterfaceInput = body, OriginalInterfaceInput = body,
FormFieldList = interfaceDto.FormFieldList // normal case is null FormFieldList = interfaceDto.FormFieldList,
QueryFieldList = interfaceDto.QueryFieldList,
HeaderFieldList = interfaceDto.HeaderFieldList
}; };
authContext.ServiceProviderRequestLog = new RequestLogDto(logRepository.AppRequestLog.RequestId) authContext.ServiceProviderRequestLog = new RequestLogDto(logRepository.AppRequestLog.RequestId)
{ {

View File

@ -27,9 +27,10 @@ public class FormParameterService : ApplicationService
/// <param name="interfaceId">接口id</param> /// <param name="interfaceId">接口id</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("FormParameter/GetParameterList")] [HttpGet("FormParameter/GetParameterList")]
public async Task<List<FormParameterItem>> GetParameterListAsync([Required] int interfaceId) public async Task<List<FormParameterItem>> GetParameterListAsync([Required] int interfaceId, FieldPosition? fieldPosition = null)
{ {
var formList = await _formParameterRepository.GetListAsync(x => x.InterfaceId == interfaceId); var formList = await _formParameterRepository.GetListAsync(x =>
x.InterfaceId == interfaceId && (fieldPosition == null || x.FieldPosition == fieldPosition));
return ObjectMapper.Map<List<FormParameterEntity>, List<FormParameterItem>>(formList); return ObjectMapper.Map<List<FormParameterEntity>, List<FormParameterItem>>(formList);
} }
@ -38,9 +39,10 @@ public class FormParameterService : ApplicationService
/// </summary> /// </summary>
/// <param name="list"></param> /// <param name="list"></param>
/// <param name="interfaceId">接口id</param> /// <param name="interfaceId">接口id</param>
internal async Task CreateParametersAsync(List<FormParameterInput> list, int interfaceId) internal async Task CreateParametersAsync(List<FormParameterInput> list, int interfaceId, FieldPosition fieldPosition)
{ {
if (list.Count <= 0) return; if (list.Count <= 0) return;
list.ForEach(x => x.FieldPosition = fieldPosition);
var formEntities = ObjectMapper.Map<List<FormParameterInput>, List<FormParameterEntity>>(list); var formEntities = ObjectMapper.Map<List<FormParameterInput>, List<FormParameterEntity>>(list);
formEntities.ForEach(x => x.InterfaceId = interfaceId); formEntities.ForEach(x => x.InterfaceId = interfaceId);
await _formParameterRepository.InsertRangeAsync(formEntities); await _formParameterRepository.InsertRangeAsync(formEntities);
@ -51,9 +53,10 @@ public class FormParameterService : ApplicationService
/// </summary> /// </summary>
/// <param name="list"></param> /// <param name="list"></param>
/// <param name="interfaceId">接口id</param> /// <param name="interfaceId">接口id</param>
internal async Task MaintainParametersAsync(List<FormParameterInput> list, int interfaceId) internal async Task MaintainParametersAsync(List<FormParameterInput> list, int interfaceId, FieldPosition fieldPosition)
{ {
if (list.Count == 0) return; if (list.Count == 0) return;
list.ForEach(x => x.FieldPosition = fieldPosition);
using var ts = TransacationHelper.GetReadCommitted(); using var ts = TransacationHelper.GetReadCommitted();
@ -68,12 +71,12 @@ public class FormParameterService : ApplicationService
.Where(x => x.OptionType == OptionType.).ToList()); .Where(x => x.OptionType == OptionType.).ToList());
await _formParameterRepository.UpdateRangeAsync(updEntities, x => new await _formParameterRepository.UpdateRangeAsync(updEntities, x => new
{ {
x.Name, x.Value, x.Description x.Name, x.Value, x.Description, x.FieldPosition
}); });
// 创建 // 创建
await CreateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList() await CreateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList()
, interfaceId); , interfaceId, fieldPosition);
ts.Complete(); ts.Complete();
} }

View File

@ -48,10 +48,10 @@ public class ParameterService : ApplicationService
[HttpGet("Parameter/GetParameterList")] [HttpGet("Parameter/GetParameterList")]
[Authorize] [Authorize]
public async Task<IEnumerable<InterfaceParameterOutputValueObject>> GetParameterListAsync([Required] bool isInPara, public async Task<IEnumerable<InterfaceParameterOutputValueObject>> GetParameterListAsync([Required] bool isInPara,
[Required] int id, bool onlyLeafs = false) [Required] int id, bool onlyLeafs = false, ParameterParaKind? paraKind = null)
{ {
var paras = await _interfaceRepository.GetParameterListByIdAsync(id, onlyLeafs); var paras = await _interfaceRepository.GetParameterListByIdAsync(id, onlyLeafs);
return paras.Where(x => x.IsInPara == isInPara); return paras.Where(x => x.IsInPara == isInPara && (paraKind == null || x.ParaKind == paraKind));
} }
/// <summary> /// <summary>
@ -287,11 +287,13 @@ public class ParameterService : ApplicationService
} }
internal async Task<bool> MaintainParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara) internal async Task<bool> MaintainParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara,
ParameterParaKind paraKind = ParameterParaKind.Normal)
{ {
#region #region
var parameters = await _parameterRepository.GetParameterListByNameAsync(interfaceId, isInPara); list.ForEach(x => x.ParaKind = paraKind);
var parameters = await _parameterRepository.GetParameterListByNameAsync(interfaceId, isInPara, paraKind);
foreach (var item in parameters) foreach (var item in parameters)
{ {
if (list.All(x => x.Id != item.Id)) if (list.All(x => x.Id != item.Id))
@ -307,9 +309,9 @@ public class ParameterService : ApplicationService
using var ts = TransacationHelper.GetReadCommitted(); using var ts = TransacationHelper.GetReadCommitted();
// 创建 // 创建
await CreateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList(), interfaceId, isInPara); await CreateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList(), interfaceId, isInPara, paraKind);
// 更新 // 更新
await UpdateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList(), interfaceId, isInPara); await UpdateParametersAsync(list.Where(x => x.OptionType == OptionType.).ToList(), interfaceId, isInPara, paraKind);
// 删除 // 删除
await DeleteParametersAsync(list.Where(x => x.OptionType == OptionType.).Select(x => x.Id).ToList()); await DeleteParametersAsync(list.Where(x => x.OptionType == OptionType.).Select(x => x.Id).ToList());
@ -328,11 +330,13 @@ public class ParameterService : ApplicationService
/// <param name="interfaceId"></param> /// <param name="interfaceId"></param>
/// <param name="isInPara"></param> /// <param name="isInPara"></param>
/// <exception cref="BusinessException"></exception> /// <exception cref="BusinessException"></exception>
internal async Task CreateParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara) internal async Task CreateParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara,
ParameterParaKind paraKind = ParameterParaKind.Normal)
{ {
if (list.Count <= 0) return; if (list.Count <= 0) return;
list.ForEach(x => x.ParaKind = paraKind);
var parameters = var parameters =
await _parameterRepository.GetListAsync(x => x.InterfaceId == interfaceId && x.IsInPara == isInPara); await _parameterRepository.GetListAsync(x => x.InterfaceId == interfaceId && x.IsInPara == isInPara && x.ParaKind == paraKind);
var tops = new List<ParameterInput>(); var tops = new List<ParameterInput>();
foreach (var item in list) foreach (var item in list)
@ -368,6 +372,7 @@ public class ParameterService : ApplicationService
entity.Alias = cur.Alias; entity.Alias = cur.Alias;
entity.InterfaceId = interfaceId; entity.InterfaceId = interfaceId;
entity.IsInPara = isInPara; entity.IsInPara = isInPara;
entity.ParaKind = paraKind;
if (!cur.Sort.IsNullOrWhiteSpace()) if (!cur.Sort.IsNullOrWhiteSpace())
{ {
@ -414,13 +419,14 @@ public class ParameterService : ApplicationService
/// <param name="list"></param> /// <param name="list"></param>
/// <param name="interfaceId"></param> /// <param name="interfaceId"></param>
/// <param name="isInPara"></param> /// <param name="isInPara"></param>
private async Task UpdateParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara) private async Task UpdateParametersAsync(List<ParameterInput> list, int interfaceId, bool isInPara,
ParameterParaKind paraKind)
{ {
if (list.Count == 0) return; if (list.Count == 0) return;
Debug.Assert(!list.Any(x => x.Id < 1), "参数id均需大于0"); Debug.Assert(!list.Any(x => x.Id < 1), "参数id均需大于0");
var parameters = var parameters =
await _parameterRepository.GetListAsync(x => x.InterfaceId == interfaceId && x.IsInPara == isInPara); await _parameterRepository.GetListAsync(x => x.InterfaceId == interfaceId && x.IsInPara == isInPara && x.ParaKind == paraKind);
var treeList = BuildTreeList(parameters, ParameterRootId); var treeList = BuildTreeList(parameters, ParameterRootId);

View File

@ -19,6 +19,11 @@ public class FormParameterItem
///</summary> ///</summary>
public string Value { get; set; } public string Value { get; set; }
///<summary>
/// 字段位置
///</summary>
public FieldPosition FieldPosition { get; set; }
///<summary> ///<summary>
/// 参数描述 /// 参数描述
///</summary> ///</summary>

View File

@ -96,4 +96,14 @@ public class TargetInterfaceSummary
/// 表单字段列表 /// 表单字段列表
/// </summary> /// </summary>
public List<InterfaceFormFieldDto> FormFieldList { get; set; } = []; public List<InterfaceFormFieldDto> FormFieldList { get; set; } = [];
/// <summary>
/// Query 字段列表
/// </summary>
public List<InterfaceFormFieldDto> QueryFieldList { get; set; } = [];
/// <summary>
/// Header 字段列表
/// </summary>
public List<InterfaceFormFieldDto> HeaderFieldList { get; set; } = [];
} }

View File

@ -0,0 +1,7 @@
namespace InterfaceForward.Domain.Shared.Enum;
public enum ParameterParaKind
{
Normal = 0,
MappingBody = 1
}

View File

@ -4,6 +4,7 @@ public static class GlobalConst
{ {
public const string FromAccount = "$FromAccount"; public const string FromAccount = "$FromAccount";
public const string FromBody = "$FromBody"; public const string FromBody = "$FromBody";
public const string FromMappingBody = "$FromMappingBody";
public const string TimeStamp = "$TimeStamp"; public const string TimeStamp = "$TimeStamp";
public const string DateTime = "$DateTime"; public const string DateTime = "$DateTime";
public const string Url = "$Url"; public const string Url = "$Url";

View File

@ -90,17 +90,37 @@ public class InterfaceOutput
public ParameterType? MappedParamType { get; set; } public ParameterType? MappedParamType { get; set; }
/// <summary> /// <summary>
/// 入参列表 /// 上游入参列表
/// </summary> /// </summary>
public List<InterfaceParameterOutputValueObject> InParameterList { get; set; } public List<InterfaceParameterOutputValueObject> InParameterList { get; set; }
/// <summary>
/// Json/Xml 请求体参数树
/// </summary>
public List<InterfaceParameterOutputValueObject> RequestBodyParameterList { get; set; }
/// <summary>
/// Form 类映射体参数树
/// </summary>
public List<InterfaceParameterOutputValueObject> MappingBodyParameterList { get; set; }
/// <summary> /// <summary>
/// 出参列表 /// 出参列表
/// </summary> /// </summary>
public List<InterfaceParameterOutputValueObject> OutParameterList { get; set; } public List<InterfaceParameterOutputValueObject> OutParameterList { get; set; }
/// <summary> /// <summary>
/// 表单列表 /// Query KV 列表
/// </summary> /// </summary>
public List<FormParameterItem> FormParameterList { get; set; } public List<FormParameterItem> QueryFormParameterList { get; set; }
/// <summary>
/// Header KV 列表
/// </summary>
public List<FormParameterItem> HeaderFormParameterList { get; set; }
/// <summary>
/// Body Form KV 列表
/// </summary>
public List<FormParameterItem> BodyFormParameterList { get; set; }
} }

View File

@ -109,7 +109,7 @@ public class InterfaceForwardQuery : BasicRepository<FixedParameterEntity>, ISco
/// <param name="isInParaMap">是入参映射</param> /// <param name="isInParaMap">是入参映射</param>
/// <returns></returns> /// <returns></returns>
public async Task<IReadOnlyCollection<InterfaceParameterMapDto>> GetParameterMapListAsync(List<int> mapIds, public async Task<IReadOnlyCollection<InterfaceParameterMapDto>> GetParameterMapListAsync(List<int> mapIds,
bool isInParaMap) bool isInParaMap, ParameterParaKind? paraKind = null)
{ {
// tips这里要拿所有节点而非仅叶子因为要构建参数树 // tips这里要拿所有节点而非仅叶子因为要构建参数树
@ -130,7 +130,7 @@ ORDER BY `b`.`Sort` ASC
select `b`.`Id` AS `Id` , `b`.`PId` AS `PId` , b.InterfaceId, `b`.`Name` AS `Name` ,b.Sort, `b`.`Type` AS `Type` , `b`.`Alias` AS `Alias` , select `b`.`Id` AS `Id` , `b`.`PId` AS `PId` , b.InterfaceId, `b`.`Name` AS `Name` ,b.Sort, `b`.`Type` AS `Type` , `b`.`Alias` AS `Alias` ,
{(isMultiMap ? "CONCAT(d.Code, '.', c.Alias)" : "c.Alias")} AS `MapAlias` , `b`.`IsRequired` AS `IsRequired` , t.FreeMap {(isMultiMap ? "CONCAT(d.Code, '.', c.Alias)" : "c.Alias")} AS `MapAlias` , `b`.`IsRequired` AS `IsRequired` , t.FreeMap
from t_interface_map t from t_interface_map t
LEFT JOIN `t_parameter` `b` ON {(isInParaMap ? "t.DownStreamId" : "t.UpStreamId")} = `b`.`InterfaceId` and b.IsInPara = {isInParaMap} AND ( `b`.`IsDeleted` = 0 ) LEFT JOIN `t_parameter` `b` ON {(isInParaMap ? "t.DownStreamId" : "t.UpStreamId")} = `b`.`InterfaceId` and b.IsInPara = {isInParaMap} {(paraKind == null ? string.Empty : $"and b.ParaKind = {(int)paraKind}")} AND ( `b`.`IsDeleted` = 0 )
Left JOIN `t_interface_map_detail` `a` ON ( `t`.`Id` = `a`.`InterfaceMapId` ) and b.id = a.ParaId and a.IsDeleted = 0 Left JOIN `t_interface_map_detail` `a` ON ( `t`.`Id` = `a`.`InterfaceMapId` ) and b.id = a.ParaId and a.IsDeleted = 0
Left JOIN `t_parameter` `c` ON ( `a`.`MappedParaId` = `c`.`Id` ) AND ( `c`.`IsDeleted` = 0 ) Left JOIN `t_parameter` `c` ON ( `a`.`MappedParaId` = `c`.`Id` ) AND ( `c`.`IsDeleted` = 0 )
Left JOIN t_interface d on c.interfaceid = d.id Left JOIN t_interface d on c.interfaceid = d.id
@ -207,15 +207,20 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
InParamTreeList = null, InParamTreeList = null,
FixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, authInterface.Id), FixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, authInterface.Id),
ReturnConfigList = await GetInterfaceReturnConfigListAsync(authInterface.Id), ReturnConfigList = await GetInterfaceReturnConfigListAsync(authInterface.Id),
FormFieldList = await GetFormFieldListAsync(authInterface.Id), FormFieldList = await GetFormFieldListAsync(authInterface.Id, FieldPosition.Body),
QueryFieldList = await GetFormFieldListAsync(authInterface.Id, FieldPosition.Query),
HeaderFieldList = await GetFormFieldListAsync(authInterface.Id, FieldPosition.Header),
}; };
} }
} }
var inParaMaps = await GetParameterMapListAsync(mapIds, true);
foreach (var map in maps) foreach (var map in maps)
{ {
var targetInterface = targetInterfaceList.First(x => map.DownStreamCode == x.Code); var targetInterface = targetInterfaceList.First(x => map.DownStreamCode == x.Code);
var paraKind = targetInterface.ContentType == ContentType.FormUrlEncoded
? ParameterParaKind.MappingBody
: ParameterParaKind.Normal;
var inParaMaps = await GetParameterMapListAsync([map.Id], true, paraKind);
var items = inParaMaps.Where(x => x.InterfaceId == targetInterface.Id).ToList(); var items = inParaMaps.Where(x => x.InterfaceId == targetInterface.Id).ToList();
var fixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, targetInterface.Id); var fixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, targetInterface.Id);
@ -235,17 +240,19 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
ReturnConfigList = await GetInterfaceReturnConfigListAsync(targetInterface.Id), ReturnConfigList = await GetInterfaceReturnConfigListAsync(targetInterface.Id),
PrefixScript = map.PrefixScript, PrefixScript = map.PrefixScript,
PostfixScript = map.PostfixScript, PostfixScript = map.PostfixScript,
FormFieldList = await GetFormFieldListAsync(targetInterface.Id), FormFieldList = await GetFormFieldListAsync(targetInterface.Id, FieldPosition.Body),
QueryFieldList = await GetFormFieldListAsync(targetInterface.Id, FieldPosition.Query),
HeaderFieldList = await GetFormFieldListAsync(targetInterface.Id, FieldPosition.Header),
}); });
} }
return context; return context;
} }
private async Task<List<InterfaceFormFieldDto>> GetFormFieldListAsync(int interfaceId) private async Task<List<InterfaceFormFieldDto>> GetFormFieldListAsync(int interfaceId, FieldPosition fieldPosition)
{ {
return await Context.Queryable<FormParameterEntity>() return await Context.Queryable<FormParameterEntity>()
.Where(x => x.InterfaceId == interfaceId) .Where(x => x.InterfaceId == interfaceId && x.FieldPosition == fieldPosition)
.Select<InterfaceFormFieldDto>() .Select<InterfaceFormFieldDto>()
.ToListAsync(); .ToListAsync();
} }
@ -260,7 +267,7 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
int interfaceId) int interfaceId)
{ {
var fixedFieldList = await Context.Queryable<FixedParameterEntity>() var fixedFieldList = await Context.Queryable<FixedParameterEntity>()
.Where(a => a.ServiceProviderId == serviceProviderId || a.InterfaceId == interfaceId) .Where(a => a.ServiceProviderId == serviceProviderId)
.Select(x => new .Select(x => new
{ {
x.ServiceProviderId, x.ServiceProviderId,

View File

@ -1,4 +1,6 @@
namespace InterfaceForward.Repositories.Parameter.Entities; using InterfaceForward.Domain.Shared.Enum;
namespace InterfaceForward.Repositories.Parameter.Entities;
///<summary> ///<summary>
/// 表单参数 /// 表单参数
@ -30,6 +32,12 @@ public class FormParameterEntity : FullAuditedAggregateRoot<int, int>
[SugarColumn(ColumnName = "Value")] [SugarColumn(ColumnName = "Value")]
public string Value { get; set; } public string Value { get; set; }
///<summary>
/// 字段位置
///</summary>
[SugarColumn(ColumnName = "FieldPosition")]
public FieldPosition FieldPosition { get; set; }
///<summary> ///<summary>
/// 参数描述 /// 参数描述
///</summary> ///</summary>

View File

@ -63,6 +63,12 @@ namespace InterfaceForward.Repositories.Interface.Entitys
[SugarColumn(ColumnName = "IsInPara")] [SugarColumn(ColumnName = "IsInPara")]
public bool IsInPara { get; set; } public bool IsInPara { get; set; }
///<summary>
/// 参数用途0普通1映射体
///</summary>
[SugarColumn(ColumnName = "ParaKind")]
public ParameterParaKind ParaKind { get; set; }
///<summary> ///<summary>
/// 参数描述 /// 参数描述
///</summary> ///</summary>

View File

@ -1,5 +1,6 @@
using InterfaceForward.Repositories.Interface.Entitys; using InterfaceForward.Repositories.Interface.Entitys;
using InterfaceForward.Repositories.Parameter.VO; using InterfaceForward.Repositories.Parameter.VO;
using InterfaceForward.Domain.Shared.Enum;
namespace InterfaceForward.Repositories.Parameter.Services; namespace InterfaceForward.Repositories.Parameter.Services;
@ -14,7 +15,8 @@ public interface IParameterRepository:IBasicRepository<ParameterEntity>
/// <param name="interfaceId"></param> /// <param name="interfaceId"></param>
/// <param name="isInPara"></param> /// <param name="isInPara"></param>
/// <returns></returns> /// <returns></returns>
Task<List<InterfaceParameterOutputValueObject>> GetParameterListByNameAsync(int interfaceId, bool isInPara); Task<List<InterfaceParameterOutputValueObject>> GetParameterListByNameAsync(int interfaceId, bool isInPara,
ParameterParaKind? paraKind = null);
/// <summary> /// <summary>
/// 保存参数序列 /// 保存参数序列

View File

@ -1,5 +1,6 @@
using InterfaceForward.Repositories.Interface.Entitys; using InterfaceForward.Repositories.Interface.Entitys;
using InterfaceForward.Repositories.Parameter.VO; using InterfaceForward.Repositories.Parameter.VO;
using InterfaceForward.Domain.Shared.Enum;
namespace InterfaceForward.Repositories.Parameter.Services; namespace InterfaceForward.Repositories.Parameter.Services;
@ -10,11 +11,12 @@ public class ParameterRepository : BasicRepository<ParameterEntity>, IScopedDepe
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<List<InterfaceParameterOutputValueObject>> GetParameterListByNameAsync(int interfaceId, public async Task<List<InterfaceParameterOutputValueObject>> GetParameterListByNameAsync(int interfaceId,
bool isInPara) bool isInPara, ParameterParaKind? paraKind = null)
{ {
var query = Context.Queryable<ParameterEntity>() var query = Context.Queryable<ParameterEntity>()
.Where(x => x.InterfaceId == interfaceId) .Where(x => x.InterfaceId == interfaceId)
.Where(x => x.IsInPara == isInPara) .Where(x => x.IsInPara == isInPara)
.WhereIF(paraKind != null, x => x.ParaKind == paraKind)
.OrderBy(x => x.Sort); .OrderBy(x => x.Sort);
return await query.Select<InterfaceParameterOutputValueObject>().ToListAsync(); return await query.Select<InterfaceParameterOutputValueObject>().ToListAsync();

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json; using InterfaceForward.Domain.Shared.Enum;
using Newtonsoft.Json;
namespace InterfaceForward.Repositories.Parameter.VO namespace InterfaceForward.Repositories.Parameter.VO
{ {
@ -34,6 +35,11 @@ namespace InterfaceForward.Repositories.Parameter.VO
///</summary> ///</summary>
public int Type { get; set; } public int Type { get; set; }
/// <summary>
/// 参数用途
/// </summary>
public ParameterParaKind ParaKind { get; set; }
/// <summary> /// <summary>
/// 是否必填 /// 是否必填
/// </summary> /// </summary>

View File

@ -1,5 +1,10 @@
using InterfaceForward.Application.Contracts.Dtos.ScriptAssistant; using InterfaceForward.Application.Contracts.Dtos.ScriptAssistant;
using InterfaceForward.Application.Contracts.ForwardCore;
using InterfaceForward.Application.ForwardCore;
using InterfaceForward.Application.Services.ScriptAssistant; using InterfaceForward.Application.Services.ScriptAssistant;
using InterfaceForward.Domain.Shared;
using InterfaceForward.Domain.Shared.Dtos;
using InterfaceForward.Domain.Shared.Enum;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace InterfaceForward.Application.Tests; namespace InterfaceForward.Application.Tests;
@ -72,3 +77,56 @@ public class ScriptAssistantServiceTests
Assert.Contains(output.Warnings, x => x.Contains("System.IO")); Assert.Contains(output.Warnings, x => x.Contains("System.IO"));
} }
} }
public class DefaultForwardFlowTests
{
[Fact]
public void SerializationBody_ShouldUseMappedInputForJson()
{
var flow = new TestForwardFlow();
var context = new ForwardCoreContext
{
TargetInterface = new InterfaceDto { ContentType = ContentType.Json },
OriginalInterfaceMappedInput = new JObject { ["orderId"] = "A001" }
};
var body = flow.SerializationBody(context);
Assert.Equal("{\"orderId\":\"A001\"}", body);
}
[Fact]
public async Task SetContentType_ShouldReplaceFromMappingBodyForFormUrlEncoded()
{
var flow = new TestForwardFlow();
var context = new ForwardCoreContext
{
TargetInterface = new InterfaceDto { ContentType = ContentType.FormUrlEncoded },
TargetInterfaceInput = "{\"orderId\":\"A001\"}",
OriginalInterfaceMappedInput = new JObject { ["orderId"] = "A001" },
FormFieldList =
[
new InterfaceFormFieldDto
{
Name = "notifyBizEventDTO",
Value = GlobalConst.FromMappingBody
}
]
};
using var message = new HttpRequestMessage();
flow.SetContentTypeForTest(context, message);
var content = await message.Content!.ReadAsStringAsync();
Assert.Equal("notifyBizEventDTO=%7B%22orderId%22%3A%22A001%22%7D", content);
}
class TestForwardFlow : DefaultForwardFlow
{
public void SetContentTypeForTest(ForwardCoreContext context, HttpRequestMessage message)
{
SetContentType(context, message);
}
}
}

View File

@ -0,0 +1,372 @@
# 方案 B 重构清单(前后对比 · 修订版)
> **前提**:全量重构,不做旧字段兼容。
> **修订要点**:先定义「一次 HTTP 请求」应保存哪些结构,再说明 **Mapping Body 仅在某些 Content-Type 下才存在**`application/json` 的请求体参数树 **本身就是映射体**,不单独拆 Mapping Body。
---
## 1. 设计顺序(先看这个)
```text
第一步 请求接口应保存什么(与 Content-Type 无关 + 有关部分分开)
Query / Header / Body 传输层 + 响应 + 基本信息
第二步 映射体放哪(由 Content-Type 决定)
Json/Xml → Body 参数树 = 映射体
Form 类 → 传输用表单 KV + 独立 Mapping Body 树(表单字段可 $FromMappingBody
```
**Mapping Body 不是每个接口都有**只有「Body或 Query走表单 KV且业务 JSON 需要单独做上游映射」时才配置。
---
## 2. 改造后:一次请求应保存什么(目标模型)
### 2.1 与 Content-Type **无关**(任何下游接口都有)
| 存储 | 表 | UI对齐 ApiTest | 说明 |
|------|-----|-------------------|------|
| **基本信息** | `t_interface` | 地址、Method、Protocol、ContentType、QPS… | 不变 |
| **Query** | `t_parameter_form`FieldPosition=**Query** | Query TabKV | 真实 URL Query不仅 FormUrlEncoded 才有 |
| **Header** | `t_parameter_form`FieldPosition=**Header** | Header TabKV | 请求头;占位符 $TimeStamp、$token 等 |
| **Response** | `t_parameter`IsInPara=false | Response 参数树 | 不变 |
| **返回配置** | `t_interface_return_config` | 返回配置 Tab | 不变 |
| **cURL 示例** | `t_interface.InParams` | cURL Tab | 不变 |
| **服务商级 Fixed** | `t_parameter_fixed`ServiceProviderId | 不在接口明细维护 | Url 占位、服务商通用 Header、脚本写入 |
### 2.2 与 Content-Type **有关**Body 区不同)
| ContentType | Body 传输层保存什么 | 映射体保存什么 | 是否需要 Mapping Body |
|-------------|---------------------|----------------|------------------------|
| **Json (3)** | **`requestBodyParameterList`** 参数树 | **同一棵树** | **否** |
| **Xml (4)** | **`requestBodyParameterList`** 参数树 | **同一棵树** | **否** |
| **FormUrlEncoded (1)** | **`bodyFormParameterList`** KV | **`mappingBodyParameterList`** 参数树 | **是** |
| **FormData (2)** | 二期:`bodyForm` 或 multipart 描述 | 若字段内嵌 JSON同 FormUrlEncoded | 二期 |
| **上游接口** | **`inParameterList`** 树(业务入参) | **同一棵树** | **否** |
### 2.3 结构总图(改造后)
```text
┌─ 基本信息 (t_interface)
任意 Content-Type ├─ queryFormParameterList (t_parameter_form, Query)
├─ headerFormParameterList (t_parameter_form, Header)
├─ Body 区 ─────────────────────────────────────┐
│ │
│ Json/Xml FormUrlEncoded │
│ requestBody bodyFormParameterList │
│ ParameterList (KV 传输) │
│ (树=映射体) mappingBodyParameterList │
│ (树=仅映射体) │
└────────────────────────────────────────────────┘
├─ outParameterList (出参树)
└─ inParams (cURL 示例)
```
### 2.4 页面 Tab下游接口 · 按 Content-Type 显示)
| Tab | Json / Xml | FormUrlEncoded |
|-----|------------|----------------|
| Query | 显示 KV | 显示 KV |
| Header | 显示 KV | 显示 KV |
| **Body** | **参数树**= 映射体) | **KV 表**(传输) |
| **Mapping Body** | **隐藏** | **显示**(参数树) |
| cURL | 显示 | 显示 |
| Response | 显示 | 显示 |
| ~~固定配置~~ | 删除(接口级) | 删除 |
与 ApiTest 对齐方式:
- ApiTest 的 Request TabJson 时常用 Raw**接口配置**里 Json 仍用 **参数树** 定义结构itfx 现有能力,且即映射体)。
- ApiTest 的 FormUrlEncodedURL Encoded KV = 我们的 **bodyForm**;映射结构不在调试页里,在接口配置里用 **Mapping Body** 维护。
---
## 3. Mapping Body 何时存在(规则定稿)
| 条件 | 结论 |
|------|------|
| `ContentType = Json``Xml` | **没有** `mappingBodyParameterList``requestBodyParameterList` 参与 `InParamRestructure` 且被 `SerializationBody` 序列化 |
| `ContentType = FormUrlEncoded` | **有** `bodyFormParameterList` + **有** `mappingBodyParameterList`;映射只对 Mapping Body 树;表单字段值可为 `$FromMappingBody` |
| Query 上也有「值为 JSON」的罕见情况 | 仍用 Mapping Body 树做映射Query KV 里字段填 `$FromMappingBody`(与 Body 表单同理) |
| 上游 `IsUpStream=true` | 始终只有 **`inParameterList`**,无 Mapping Body、无 bodyForm |
**占位符**
| 占位符 | 用于 | Content-Type |
|--------|------|----------------|
| `$FromMappingBody` | Form 的 Query/Body及必要时 Header字段引用映射结果 | **仅 Form 类** |
| `$FromBody` | **删除**下游表单中的用法 | — |
| Json Body 树字段 | 直接映射到 alias**不需要**占位符塞整段 JSON | Json / Xml |
---
## 4. 数据库(修订)
### 4.1 `t_parameter_form` — 传输层 KV
**新增** `FieldPosition`Header=1, Body=2, Query=3。
| FieldPosition | 何时有数据 |
|---------------|------------|
| Query | 任意 Content-Type有 URL 参数就配 |
| Header | 任意 Content-Type |
| Body | **主要 FormUrlEncoded**Json/Xml 通常 **无 body 行**Body 在参数树) |
### 4.2 `t_parameter` — 参数树
**新增** `ParaKind`
| ParaKind | 含义 | 用于 |
|----------|------|------|
| **0 Normal** | 默认 | 出参;**上游入参**Json/Xml 的 **RequestBody** |
| **1 MappingBody** | 仅映射、不直接当 HTTP Json Body | **仅** FormUrlEncoded及日后 FormData且需要独立映射体时 |
| 列表字段 | ParaKind | IsInPara |
|----------|----------|----------|
| `requestBodyParameterList` | Normal或枚举值 RequestBody=0 | true |
| `mappingBodyParameterList` | **MappingBody** | true |
| `outParameterList` | Normal | false |
**删除的用法**
- 不再用「一棵 in 树」同时表示 Form 映射 + Json Body改造前混用
### 4.3 `t_parameter_fixed`
- **删**:接口明细维护接口级 Header/Query/Body KV。
- **留**服务商级、Url、脚本动态 Header。
---
## 5. SaveInterface / GetInterfaceById请求结构
`POST /api/Interface/SaveInterface` · 响应为接口 `id`number
`GetInterfaceById` 字段与保存一致。已删除:`formParameterList`、下游 `inParameterList`
### 5.1 根对象
| 字段 | 说明 |
|------|------|
| `optionType` | `1` 增 / `3` 改 / `2` 删 |
| `isUpStream` | `true` 上游 / `false` 下游 |
| `serviceProviderId` | 上游 `0` |
| `id` | 改、删必填 |
| `name`, `requestAddress`, `requestProtocol`, `requestMethod`, `contentType` | 基本信息 |
| `code`, `category`, `description`, `isBatch`, `limit`, `responseContentType`, `flowCode`, `isDisableAuth`, `qps`, `mappedParamType`, `inParams` | 可选 |
| `queryFormParameterList` | Query KV`fieldPosition=3` |
| `headerFormParameterList` | Header KV`fieldPosition=1` |
| `outParameterList` | 出参树 |
| `requestBodyParameterList` | **仅下游 Json/Xml**Body 树 = 映射体 |
| `bodyFormParameterList` | **仅下游 FormUrlEncoded**Body KV |
| `mappingBodyParameterList` | **仅下游 FormUrlEncoded**,映射树 |
| `inParameterList` | **仅上游**入参树 |
### 5.2 Body 字段互斥
| 场景 | 传 | 不传 |
|------|-----|------|
| 下游 Json/Xml (`contentType` 3/4) | `requestBodyParameterList` | `bodyForm*`、`mappingBody*`、`inParameterList` |
| 下游 FormUrlEncoded (`1`) | `bodyFormParameterList` + `mappingBodyParameterList` | `requestBody*`、`inParameterList` |
| 上游 | `inParameterList` | `requestBody*`、`bodyForm*`、`mappingBody*` |
### 5.3 子结构
**FormParameterInput**Query / Header / Body 行)
```json
{ "optionType": 1, "id": 0, "name": "字段名", "value": "$FromMappingBody", "description": "", "fieldPosition": 2 }
```
`fieldPosition``1` Header · `2` Body · `3` Query。Form 业务 JSON 字段用 `$FromMappingBody`
**ParameterInput**树节点requestBody / mappingBody / in / out
```json
{ "optionType": 1, "id": 1, "pId": 0, "name": "order", "alias": "order", "type": 3, "isRequired": true, "sort": "1", "paraKind": 0 }
```
`paraKind``0` NormalrequestBody、上游 in、出参· `1` MappingBody仅 mappingBody 列表)。新增时 `id`/`pId` 需表达父子关系。
---
## 5.4 示例
**下游 · Json**`contentType: 3`
```json
{
"optionType": 1,
"isUpStream": false,
"serviceProviderId": 5,
"name": "创建订单",
"requestAddress": "https://api.example.com/order",
"requestProtocol": 2,
"requestMethod": 2,
"contentType": 3,
"queryFormParameterList": [
{ "optionType": 1, "id": 0, "name": "access_token", "value": "$FromAccount", "fieldPosition": 3 }
],
"headerFormParameterList": [],
"requestBodyParameterList": [
{ "optionType": 1, "id": 1, "pId": 0, "name": "orderId", "alias": "orderId", "type": 1, "paraKind": 0 }
],
"outParameterList": []
}
```
**下游 · FormUrlEncoded**`contentType: 1`
```json
{
"optionType": 3,
"isUpStream": false,
"serviceProviderId": 5,
"id": 1001,
"name": "1688回调",
"requestAddress": "https://gw.open.1688.com/openapi/http",
"requestProtocol": 2,
"requestMethod": 2,
"contentType": 1,
"queryFormParameterList": [
{ "optionType": 3, "id": 11, "name": "_aop_timestamp", "value": "$TimeStamp", "description": "ms", "fieldPosition": 3 }
],
"headerFormParameterList": [],
"bodyFormParameterList": [
{ "optionType": 3, "id": 10, "name": "notifyBizEventDTO", "value": "$FromMappingBody", "fieldPosition": 2 },
{ "optionType": 3, "id": 13, "name": "_aop_signature", "value": "", "fieldPosition": 2 }
],
"mappingBodyParameterList": [
{ "optionType": 3, "id": 1, "pId": 0, "name": "order", "alias": "order", "type": 3, "paraKind": 1 },
{ "optionType": 3, "id": 2, "pId": 1, "name": "orderId", "alias": "orderId", "type": 1, "paraKind": 1 }
],
"outParameterList": []
}
```
**上游**`isUpStream: true`
```json
{
"optionType": 1,
"isUpStream": true,
"serviceProviderId": 0,
"name": "标准创建订单",
"requestAddress": "/api/order/create",
"requestProtocol": 2,
"requestMethod": 2,
"contentType": 3,
"queryFormParameterList": [],
"headerFormParameterList": [],
"inParameterList": [
{ "optionType": 1, "id": 1, "pId": 0, "name": "order", "alias": "order", "type": 3, "paraKind": 0 }
],
"outParameterList": []
}
```
---
## 6. 转发逻辑(修订)
### 6.1 映射树来源InParamTreeList
| ContentType | InParamTreeList 来源 |
|-------------|----------------------|
| Json / Xml | **`requestBodyParameterList`**(整棵树) |
| FormUrlEncoded | **`mappingBodyParameterList`** |
| 上游 | 系统接口 **inParameterList**(映射配置页不变) |
### 6.2 HTTP 组装
| 部分 | 来源 |
|------|------|
| URL Query | `queryFormParameterList`+ 服务商 Fixed Query |
| Header | `headerFormParameterList`+ 服务商 Fixed Header |
| Body Json | `Serialize(requestBodyParameterList` 映射结果 `)` |
| Body FormUrlEncoded | 仅 **`bodyFormParameterList`**;其中 `$FromMappingBody` ← 映射结果序列化 |
| Body Xml | 同 JsonXml 序列化 |
### 6.3 流程对比(两张)
**Json 下游(无 Mapping Body**
```text
上游 JSON → Restructure(requestBodyParameterList) → OriginalInterfaceMappedInput
→ ReplacePlaceholder(Query/Header form + Fixed)
→ SerializationBody(Json) → HTTP Body
→ 拼 Query / Header
```
**FormUrlEncoded 下游(有 Mapping Body**
```text
上游 JSON → Restructure(mappingBodyParameterList) → OriginalInterfaceMappedInput
→ ReplacePlaceholder含 bodyForm 的 $FromMappingBody
→ bodyForm 字典 → HTTP Body
→ 拼 Query / Header
```
---
## 7. 前后对比总表(增删改)
### 7.1 删除了什么
| 类别 | 内容 |
|------|------|
| API | 下游 `formParameterList`、下游笼统的 `inParameterList`(拆开后互斥) |
| UI | 固定配置 Tab接口级 Fixed API |
| 语义 | 所有下游共用一棵 in 树Form 无 positionJson 也搞 Mapping Body Tab |
| 占位 | 下游表单用 `$FromBody` 表示整段 JSON |
### 7.2 新增了什么
| 类别 | 内容 |
|------|------|
| 表 | `t_parameter_form.FieldPosition``t_parameter.ParaKind` |
| API | `queryFormParameterList`、`headerFormParameterList`**全类型共有** |
| API | `bodyFormParameterList`**仅 Form 类** |
| API | `requestBodyParameterList`**仅 Json/Xml**=映射体) |
| API | `mappingBodyParameterList`**仅 Form 类** |
| UI | Header TabForm 时 Mapping Body TabJson 时 Body=树 |
| 占位 | `$FromMappingBody`**仅 Form KV** |
### 7.3 改了什么
| 项 | 改造前 | 改造后 |
|----|--------|--------|
| Query | 假 Query实为 form body | 真 Query KV**所有 Content-Type** |
| Body Tab | 永远是参数树 | **Json=树****Form=KV** |
| 映射体 | 永远 in 树 | **Json=Body 树****Form=Mapping Body 树** |
| 保存 | form + in 混传 | 按 ContentType 传 **互斥** 的 Body 相关列表 |
| 映射 SQL | 下游 IsInPara 全量 | JsonRequestBody 树Form**ParaKind=MappingBody** |
---
## 8. Content-Type 全量对照(改造后)
| ContentType | queryForm | headerForm | bodyForm | requestBody 树 | mappingBody 树 | 映射目标 | HTTP Body |
|-------------|-----------|------------|----------|----------------|----------------|----------|-----------|
| **Json** | 可选 | 可选 | **不用** | **必填** | **不用** | requestBody 树 | 序列化该树 |
| **Xml** | 可选 | 可选 | **不用** | **必填** | **不用** | requestBody 树 | Xml 序列化 |
| **FormUrlEncoded** | 常用 | 常用 | **必填** | **不用** | **必填** | mappingBody 树 | bodyForm 字典 |
| **FormData** | 二期 | 二期 | 二期 | **不用** | 二期 | 二期 | 二期 |
| **上游 Json** | — | — | — | **inParameterList** | — | in 树 | — |
---
## 9. 一句话
| | 改造前 | 改造后 |
|---|--------|--------|
| 先保存什么 | form + in 混在一起 | **先** Query/Header通用+ **再按 Content-Type 存 Body** |
| Json | in 树 = 映射 + Body | **requestBody 树 = 映射体**,不要 Mapping Body |
| Form | in 树 + form 表 | **bodyForm 传输** + **mappingBody 映射** |
| Mapping Body | (之前文档)每个下游都有 | **仅 Form 类需要** |
---
*文档版本2026-06-02 · 含 SaveInterface 结构与示例§5*