From d70c9545a6d25423ec7f19d31ce967d8e65b0bee Mon Sep 17 00:00:00 2001
From: xiaolipro <2357729423@qq.com>
Date: Wed, 3 Jun 2026 14:49:58 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Dtos/Interface/ParameterInput.cs | 5 +
.../Dtos/Interface/SaveInterfaceInput.cs | 26 +-
.../ForwardCore/ForwardCoreContext.cs | 10 +
.../ForwardCore/DefaultForwardFlow.cs | 56 ++-
.../Services/Interface/InterfaceService.cs | 142 ++++++-
.../Services/InterfaceForwardCommon.cs | 6 +-
.../Parameter/FormParameterService.cs | 15 +-
.../Services/Parameter/ParameterService.cs | 26 +-
.../Dtos/FormParameterItem.cs | 5 +
.../Dtos/ForwardCoreContextCache.cs | 10 +
.../Enum/ParameterParaKind.cs | 7 +
.../GlobalConst.cs | 1 +
.../Interface/ValueObjects/InterfaceOutput.cs | 26 +-
.../InterfaceForwardQuery.cs | 23 +-
.../Parameter/Entities/FormParameterEntity.cs | 10 +-
.../Parameter/Entities/ParameterEntity.cs | 6 +
.../Services/IParameterRepository.cs | 4 +-
.../Parameter/Services/ParameterRepository.cs | 4 +-
.../VO/InterfaceParameterOutputValueObject.cs | 8 +-
.../UnitTest1.cs | 58 +++
方案B-重构清单-前后对比.md | 372 ++++++++++++++++++
21 files changed, 756 insertions(+), 64 deletions(-)
create mode 100644 src/InterfaceForward.Domain.Shared/Enum/ParameterParaKind.cs
create mode 100644 方案B-重构清单-前后对比.md
diff --git a/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterInput.cs
index ff0d757..2e07aa4 100644
--- a/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterInput.cs
+++ b/src/InterfaceForward.Application.Contracts/Dtos/Interface/ParameterInput.cs
@@ -42,6 +42,11 @@ namespace InterfaceForward.Application.Contracts.Dtos.Interface
///
public ParameterType Type { get; set; }
+ ///
+ /// 参数用途
+ ///
+ public ParameterParaKind ParaKind { get; set; }
+
///
/// 是否必填
///
diff --git a/src/InterfaceForward.Application.Contracts/Dtos/Interface/SaveInterfaceInput.cs b/src/InterfaceForward.Application.Contracts/Dtos/Interface/SaveInterfaceInput.cs
index eb7d5b3..1c64720 100644
--- a/src/InterfaceForward.Application.Contracts/Dtos/Interface/SaveInterfaceInput.cs
+++ b/src/InterfaceForward.Application.Contracts/Dtos/Interface/SaveInterfaceInput.cs
@@ -106,17 +106,37 @@ public class SaveInterfaceInput
public ParameterType? MappedParamType { get; set; }
///
- /// 入参列表
+ /// 上游入参列表
///
public List InParameterList { get; set; } = new();
+ ///
+ /// Json/Xml 请求体参数树
+ ///
+ public List RequestBodyParameterList { get; set; } = new();
+
+ ///
+ /// Form 类映射体参数树
+ ///
+ public List MappingBodyParameterList { get; set; } = new();
+
///
/// 出参列表
///
public List OutParameterList { get; set; } = new();
///
- /// 表单列表
+ /// Query KV 列表
///
- public List FormParameterList { get; set; } = new();
+ public List QueryFormParameterList { get; set; } = new();
+
+ ///
+ /// Header KV 列表
+ ///
+ public List HeaderFormParameterList { get; set; } = new();
+
+ ///
+ /// Body Form KV 列表
+ ///
+ public List BodyFormParameterList { get; set; } = new();
}
\ No newline at end of file
diff --git a/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs b/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs
index 6b65a94..3070152 100644
--- a/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs
+++ b/src/InterfaceForward.Application.Contracts/ForwardCore/ForwardCoreContext.cs
@@ -31,6 +31,16 @@ public class ForwardCoreContext
///
public List FormFieldList { get; set; } = [];
+ ///
+ /// Query 字段列表
+ ///
+ public List QueryFieldList { get; set; } = [];
+
+ ///
+ /// Header 字段列表
+ ///
+ public List HeaderFieldList { get; set; } = [];
+
///
/// 入参映射列表(树形结构)
///
diff --git a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs
index 0d2f375..b457827 100644
--- a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs
+++ b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs
@@ -279,6 +279,12 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
// 将参数绑定到query
var builder = new UriBuilder(targetInterface.RequestAddress);
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))
{
query.Remove(item.FieldName);
@@ -293,6 +299,16 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
SetContentType(context, msg);
// 将参数绑定到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))
{
//msg.Content.Headers.TryAddWithoutValidation(item.FieldName, item.FieldValue);
@@ -399,7 +415,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
Dictionary nameValueCollection;
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;
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)
{
- item.Value = item.Value switch
- {
- 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 // 不处理
- };
+ item.Value = ReplaceFormPlaceholder(item.Name, item.Value, item.Description);
}
// 绑定body部分的字段
@@ -551,6 +568,21 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
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)
diff --git a/src/InterfaceForward.Application/Services/Interface/InterfaceService.cs b/src/InterfaceForward.Application/Services/Interface/InterfaceService.cs
index d448bd8..96e6f54 100644
--- a/src/InterfaceForward.Application/Services/Interface/InterfaceService.cs
+++ b/src/InterfaceForward.Application/Services/Interface/InterfaceService.cs
@@ -109,12 +109,22 @@ public class InterfaceService : ApplicationService
// 构造参数
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.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;
}
@@ -133,16 +143,9 @@ public class InterfaceService : ApplicationService
[HttpPost("Interface/SaveInterface")]
public async Task SaveInterfaceAsync(SaveInterfaceInput input)
{
- input.InParameterList.ForEach(x =>
- {
- if (x.PId == 0 && x.Type == ParameterType.Array && x.Name == GlobalConst.RootParameterName)
- x.Name = string.Empty;
- });
- input.OutParameterList.ForEach(x =>
- {
- if (x.PId == 0 && x.Type == ParameterType.Array && x.Name == GlobalConst.RootParameterName)
- x.Name = string.Empty;
- });
+ NormalizeRootArrayName(GetInputParameterList(input));
+ NormalizeRootArrayName(input.OutParameterList);
+ ValidateBodyLists(input);
switch (input.OptionType)
{
@@ -209,7 +212,7 @@ public class InterfaceService : ApplicationService
// 校验参数合法性
ParameterService.ValidateParameter(
JsonConvert.DeserializeObject>(
- JsonConvert.SerializeObject(input.InParameterList))!);
+ JsonConvert.SerializeObject(GetInputParameterList(input)))!);
ParameterService.ValidateParameter(
JsonConvert.DeserializeObject>(
JsonConvert.SerializeObject(input.OutParameterList))!);
@@ -248,11 +251,11 @@ public class InterfaceService : ApplicationService
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 FormParameterService.CreateParametersAsync(input.FormParameterList, interfaceId);
+ await CreateFormParametersAsync(input, interfaceId);
// 接口代码生成时并发兜底方案
if (await InterfaceRepository.CountAsync(x => x.Code.Equals(interfaceEntity.Code)) > 1)
@@ -321,14 +324,119 @@ public class InterfaceService : ApplicationService
});
// 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);
// step3、更新表单参数
- await FormParameterService.MaintainParametersAsync(input.FormParameterList, interfaceEntity.Id);
+ await MaintainFormParametersAsync(input, interfaceEntity.Id);
// 提交事务
ts.Complete();
}
+
+ private static List 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 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);
+ }
}
\ No newline at end of file
diff --git a/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs b/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs
index 04e95c6..3728f70 100644
--- a/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs
+++ b/src/InterfaceForward.Application/Services/InterfaceForwardCommon.cs
@@ -105,6 +105,8 @@ public class InterfaceForwardCommon(
AccountFieldList = accountFieldCache,
ReturnConfigList = item.ReturnConfigList,
FormFieldList = item.FormFieldList,
+ QueryFieldList = item.QueryFieldList,
+ HeaderFieldList = item.HeaderFieldList,
InParamTreeList = item.InParamTreeList,
PrefixScript = item.PrefixScript,
PostfixScript = item.PostfixScript,
@@ -192,7 +194,9 @@ public class InterfaceForwardCommon(
PrefixScript = interfaceDto.PrefixScript,
PostfixScript = interfaceDto.PostfixScript,
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)
{
diff --git a/src/InterfaceForward.Application/Services/Parameter/FormParameterService.cs b/src/InterfaceForward.Application/Services/Parameter/FormParameterService.cs
index ce11324..a29425e 100644
--- a/src/InterfaceForward.Application/Services/Parameter/FormParameterService.cs
+++ b/src/InterfaceForward.Application/Services/Parameter/FormParameterService.cs
@@ -27,9 +27,10 @@ public class FormParameterService : ApplicationService
/// 接口id
///
[HttpGet("FormParameter/GetParameterList")]
- public async Task> GetParameterListAsync([Required] int interfaceId)
+ public async Task> 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>(formList);
}
@@ -38,9 +39,10 @@ public class FormParameterService : ApplicationService
///
///
/// 接口id
- internal async Task CreateParametersAsync(List list, int interfaceId)
+ internal async Task CreateParametersAsync(List list, int interfaceId, FieldPosition fieldPosition)
{
if (list.Count <= 0) return;
+ list.ForEach(x => x.FieldPosition = fieldPosition);
var formEntities = ObjectMapper.Map, List>(list);
formEntities.ForEach(x => x.InterfaceId = interfaceId);
await _formParameterRepository.InsertRangeAsync(formEntities);
@@ -51,9 +53,10 @@ public class FormParameterService : ApplicationService
///
///
/// 接口id
- internal async Task MaintainParametersAsync(List list, int interfaceId)
+ internal async Task MaintainParametersAsync(List list, int interfaceId, FieldPosition fieldPosition)
{
if (list.Count == 0) return;
+ list.ForEach(x => x.FieldPosition = fieldPosition);
using var ts = TransacationHelper.GetReadCommitted();
@@ -68,12 +71,12 @@ public class FormParameterService : ApplicationService
.Where(x => x.OptionType == OptionType.修改).ToList());
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()
- , interfaceId);
+ , interfaceId, fieldPosition);
ts.Complete();
}
diff --git a/src/InterfaceForward.Application/Services/Parameter/ParameterService.cs b/src/InterfaceForward.Application/Services/Parameter/ParameterService.cs
index 73179a0..d0da268 100644
--- a/src/InterfaceForward.Application/Services/Parameter/ParameterService.cs
+++ b/src/InterfaceForward.Application/Services/Parameter/ParameterService.cs
@@ -48,10 +48,10 @@ public class ParameterService : ApplicationService
[HttpGet("Parameter/GetParameterList")]
[Authorize]
public async Task> 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);
- return paras.Where(x => x.IsInPara == isInPara);
+ return paras.Where(x => x.IsInPara == isInPara && (paraKind == null || x.ParaKind == paraKind));
}
///
@@ -287,11 +287,13 @@ public class ParameterService : ApplicationService
}
- internal async Task MaintainParametersAsync(List list, int interfaceId, bool isInPara)
+ internal async Task MaintainParametersAsync(List list, int interfaceId, bool isInPara,
+ ParameterParaKind paraKind = ParameterParaKind.Normal)
{
#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)
{
if (list.All(x => x.Id != item.Id))
@@ -307,9 +309,9 @@ public class ParameterService : ApplicationService
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());
@@ -328,11 +330,13 @@ public class ParameterService : ApplicationService
///
///
///
- internal async Task CreateParametersAsync(List list, int interfaceId, bool isInPara)
+ internal async Task CreateParametersAsync(List list, int interfaceId, bool isInPara,
+ ParameterParaKind paraKind = ParameterParaKind.Normal)
{
if (list.Count <= 0) return;
+ list.ForEach(x => x.ParaKind = paraKind);
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();
foreach (var item in list)
@@ -368,6 +372,7 @@ public class ParameterService : ApplicationService
entity.Alias = cur.Alias;
entity.InterfaceId = interfaceId;
entity.IsInPara = isInPara;
+ entity.ParaKind = paraKind;
if (!cur.Sort.IsNullOrWhiteSpace())
{
@@ -414,13 +419,14 @@ public class ParameterService : ApplicationService
///
///
///
- private async Task UpdateParametersAsync(List list, int interfaceId, bool isInPara)
+ private async Task UpdateParametersAsync(List list, int interfaceId, bool isInPara,
+ ParameterParaKind paraKind)
{
if (list.Count == 0) return;
Debug.Assert(!list.Any(x => x.Id < 1), "参数id均需大于0");
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);
diff --git a/src/InterfaceForward.Domain.Shared/Dtos/FormParameterItem.cs b/src/InterfaceForward.Domain.Shared/Dtos/FormParameterItem.cs
index f273364..0509ebd 100644
--- a/src/InterfaceForward.Domain.Shared/Dtos/FormParameterItem.cs
+++ b/src/InterfaceForward.Domain.Shared/Dtos/FormParameterItem.cs
@@ -19,6 +19,11 @@ public class FormParameterItem
///
public string Value { get; set; }
+ ///
+ /// 字段位置
+ ///
+ public FieldPosition FieldPosition { get; set; }
+
///
/// 参数描述
///
diff --git a/src/InterfaceForward.Domain.Shared/Dtos/ForwardCoreContextCache.cs b/src/InterfaceForward.Domain.Shared/Dtos/ForwardCoreContextCache.cs
index 362b234..b78ef84 100644
--- a/src/InterfaceForward.Domain.Shared/Dtos/ForwardCoreContextCache.cs
+++ b/src/InterfaceForward.Domain.Shared/Dtos/ForwardCoreContextCache.cs
@@ -96,4 +96,14 @@ public class TargetInterfaceSummary
/// 表单字段列表
///
public List FormFieldList { get; set; } = [];
+
+ ///
+ /// Query 字段列表
+ ///
+ public List QueryFieldList { get; set; } = [];
+
+ ///
+ /// Header 字段列表
+ ///
+ public List HeaderFieldList { get; set; } = [];
}
\ No newline at end of file
diff --git a/src/InterfaceForward.Domain.Shared/Enum/ParameterParaKind.cs b/src/InterfaceForward.Domain.Shared/Enum/ParameterParaKind.cs
new file mode 100644
index 0000000..2079fac
--- /dev/null
+++ b/src/InterfaceForward.Domain.Shared/Enum/ParameterParaKind.cs
@@ -0,0 +1,7 @@
+namespace InterfaceForward.Domain.Shared.Enum;
+
+public enum ParameterParaKind
+{
+ Normal = 0,
+ MappingBody = 1
+}
diff --git a/src/InterfaceForward.Domain.Shared/GlobalConst.cs b/src/InterfaceForward.Domain.Shared/GlobalConst.cs
index b42c910..e18091e 100644
--- a/src/InterfaceForward.Domain.Shared/GlobalConst.cs
+++ b/src/InterfaceForward.Domain.Shared/GlobalConst.cs
@@ -4,6 +4,7 @@ public static class GlobalConst
{
public const string FromAccount = "$FromAccount";
public const string FromBody = "$FromBody";
+ public const string FromMappingBody = "$FromMappingBody";
public const string TimeStamp = "$TimeStamp";
public const string DateTime = "$DateTime";
public const string Url = "$Url";
diff --git a/src/InterfaceForward.Repositories/Interface/ValueObjects/InterfaceOutput.cs b/src/InterfaceForward.Repositories/Interface/ValueObjects/InterfaceOutput.cs
index 9ffc5fc..32dcdcf 100644
--- a/src/InterfaceForward.Repositories/Interface/ValueObjects/InterfaceOutput.cs
+++ b/src/InterfaceForward.Repositories/Interface/ValueObjects/InterfaceOutput.cs
@@ -90,17 +90,37 @@ public class InterfaceOutput
public ParameterType? MappedParamType { get; set; }
///
- /// 入参列表
+ /// 上游入参列表
///
public List InParameterList { get; set; }
+ ///
+ /// Json/Xml 请求体参数树
+ ///
+ public List RequestBodyParameterList { get; set; }
+
+ ///
+ /// Form 类映射体参数树
+ ///
+ public List MappingBodyParameterList { get; set; }
+
///
/// 出参列表
///
public List OutParameterList { get; set; }
///
- /// 表单列表
+ /// Query KV 列表
///
- public List FormParameterList { get; set; }
+ public List QueryFormParameterList { get; set; }
+
+ ///
+ /// Header KV 列表
+ ///
+ public List HeaderFormParameterList { get; set; }
+
+ ///
+ /// Body Form KV 列表
+ ///
+ public List BodyFormParameterList { get; set; }
}
\ No newline at end of file
diff --git a/src/InterfaceForward.Repositories/InterfaceForwardQuery.cs b/src/InterfaceForward.Repositories/InterfaceForwardQuery.cs
index 2c457b9..36bf31c 100644
--- a/src/InterfaceForward.Repositories/InterfaceForwardQuery.cs
+++ b/src/InterfaceForward.Repositories/InterfaceForwardQuery.cs
@@ -109,7 +109,7 @@ public class InterfaceForwardQuery : BasicRepository, ISco
/// 是入参映射
///
public async Task> GetParameterMapListAsync(List mapIds,
- bool isInParaMap)
+ bool isInParaMap, ParameterParaKind? paraKind = null)
{
// 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` ,
{(isMultiMap ? "CONCAT(d.Code, '.', c.Alias)" : "c.Alias")} AS `MapAlias` , `b`.`IsRequired` AS `IsRequired` , t.FreeMap
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_parameter` `c` ON ( `a`.`MappedParaId` = `c`.`Id` ) AND ( `c`.`IsDeleted` = 0 )
Left JOIN t_interface d on c.interfaceid = d.id
@@ -207,15 +207,20 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
InParamTreeList = null,
FixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, 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)
{
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 fixedFieldList = await GetAllFixedFieldListAsync(serviceProvider.Id, targetInterface.Id);
@@ -235,17 +240,19 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
ReturnConfigList = await GetInterfaceReturnConfigListAsync(targetInterface.Id),
PrefixScript = map.PrefixScript,
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;
}
- private async Task> GetFormFieldListAsync(int interfaceId)
+ private async Task> GetFormFieldListAsync(int interfaceId, FieldPosition fieldPosition)
{
return await Context.Queryable()
- .Where(x => x.InterfaceId == interfaceId)
+ .Where(x => x.InterfaceId == interfaceId && x.FieldPosition == fieldPosition)
.Select()
.ToListAsync();
}
@@ -260,7 +267,7 @@ ORDER BY `b`.`Sort` ASC").ToListAsync();
int interfaceId)
{
var fixedFieldList = await Context.Queryable()
- .Where(a => a.ServiceProviderId == serviceProviderId || a.InterfaceId == interfaceId)
+ .Where(a => a.ServiceProviderId == serviceProviderId)
.Select(x => new
{
x.ServiceProviderId,
diff --git a/src/InterfaceForward.Repositories/Parameter/Entities/FormParameterEntity.cs b/src/InterfaceForward.Repositories/Parameter/Entities/FormParameterEntity.cs
index b23c013..0906744 100644
--- a/src/InterfaceForward.Repositories/Parameter/Entities/FormParameterEntity.cs
+++ b/src/InterfaceForward.Repositories/Parameter/Entities/FormParameterEntity.cs
@@ -1,4 +1,6 @@
-namespace InterfaceForward.Repositories.Parameter.Entities;
+using InterfaceForward.Domain.Shared.Enum;
+
+namespace InterfaceForward.Repositories.Parameter.Entities;
///
/// 表单参数
@@ -30,6 +32,12 @@ public class FormParameterEntity : FullAuditedAggregateRoot
[SugarColumn(ColumnName = "Value")]
public string Value { get; set; }
+ ///
+ /// 字段位置
+ ///
+ [SugarColumn(ColumnName = "FieldPosition")]
+ public FieldPosition FieldPosition { get; set; }
+
///
/// 参数描述
///
diff --git a/src/InterfaceForward.Repositories/Parameter/Entities/ParameterEntity.cs b/src/InterfaceForward.Repositories/Parameter/Entities/ParameterEntity.cs
index 1727b5a..f26af58 100644
--- a/src/InterfaceForward.Repositories/Parameter/Entities/ParameterEntity.cs
+++ b/src/InterfaceForward.Repositories/Parameter/Entities/ParameterEntity.cs
@@ -63,6 +63,12 @@ namespace InterfaceForward.Repositories.Interface.Entitys
[SugarColumn(ColumnName = "IsInPara")]
public bool IsInPara { get; set; }
+ ///
+ /// 参数用途:0普通;1映射体
+ ///
+ [SugarColumn(ColumnName = "ParaKind")]
+ public ParameterParaKind ParaKind { get; set; }
+
///
/// 参数描述
///
diff --git a/src/InterfaceForward.Repositories/Parameter/Services/IParameterRepository.cs b/src/InterfaceForward.Repositories/Parameter/Services/IParameterRepository.cs
index 13f8b2f..b4ea21a 100644
--- a/src/InterfaceForward.Repositories/Parameter/Services/IParameterRepository.cs
+++ b/src/InterfaceForward.Repositories/Parameter/Services/IParameterRepository.cs
@@ -1,5 +1,6 @@
using InterfaceForward.Repositories.Interface.Entitys;
using InterfaceForward.Repositories.Parameter.VO;
+using InterfaceForward.Domain.Shared.Enum;
namespace InterfaceForward.Repositories.Parameter.Services;
@@ -14,7 +15,8 @@ public interface IParameterRepository:IBasicRepository
///
///
///
- Task> GetParameterListByNameAsync(int interfaceId, bool isInPara);
+ Task> GetParameterListByNameAsync(int interfaceId, bool isInPara,
+ ParameterParaKind? paraKind = null);
///
/// 保存参数序列
diff --git a/src/InterfaceForward.Repositories/Parameter/Services/ParameterRepository.cs b/src/InterfaceForward.Repositories/Parameter/Services/ParameterRepository.cs
index b0934ab..3411a8d 100644
--- a/src/InterfaceForward.Repositories/Parameter/Services/ParameterRepository.cs
+++ b/src/InterfaceForward.Repositories/Parameter/Services/ParameterRepository.cs
@@ -1,5 +1,6 @@
using InterfaceForward.Repositories.Interface.Entitys;
using InterfaceForward.Repositories.Parameter.VO;
+using InterfaceForward.Domain.Shared.Enum;
namespace InterfaceForward.Repositories.Parameter.Services;
@@ -10,11 +11,12 @@ public class ParameterRepository : BasicRepository, IScopedDepe
{
///
public async Task> GetParameterListByNameAsync(int interfaceId,
- bool isInPara)
+ bool isInPara, ParameterParaKind? paraKind = null)
{
var query = Context.Queryable()
.Where(x => x.InterfaceId == interfaceId)
.Where(x => x.IsInPara == isInPara)
+ .WhereIF(paraKind != null, x => x.ParaKind == paraKind)
.OrderBy(x => x.Sort);
return await query.Select().ToListAsync();
diff --git a/src/InterfaceForward.Repositories/Parameter/VO/InterfaceParameterOutputValueObject.cs b/src/InterfaceForward.Repositories/Parameter/VO/InterfaceParameterOutputValueObject.cs
index c281de1..a92c6c1 100644
--- a/src/InterfaceForward.Repositories/Parameter/VO/InterfaceParameterOutputValueObject.cs
+++ b/src/InterfaceForward.Repositories/Parameter/VO/InterfaceParameterOutputValueObject.cs
@@ -1,4 +1,5 @@
-using Newtonsoft.Json;
+using InterfaceForward.Domain.Shared.Enum;
+using Newtonsoft.Json;
namespace InterfaceForward.Repositories.Parameter.VO
{
@@ -34,6 +35,11 @@ namespace InterfaceForward.Repositories.Parameter.VO
///
public int Type { get; set; }
+ ///
+ /// 参数用途
+ ///
+ public ParameterParaKind ParaKind { get; set; }
+
///
/// 是否必填
///
diff --git a/tests/InterfaceForward.Application.Tests/UnitTest1.cs b/tests/InterfaceForward.Application.Tests/UnitTest1.cs
index 0aee79f..6df47f4 100644
--- a/tests/InterfaceForward.Application.Tests/UnitTest1.cs
+++ b/tests/InterfaceForward.Application.Tests/UnitTest1.cs
@@ -1,5 +1,10 @@
using InterfaceForward.Application.Contracts.Dtos.ScriptAssistant;
+using InterfaceForward.Application.Contracts.ForwardCore;
+using InterfaceForward.Application.ForwardCore;
using InterfaceForward.Application.Services.ScriptAssistant;
+using InterfaceForward.Domain.Shared;
+using InterfaceForward.Domain.Shared.Dtos;
+using InterfaceForward.Domain.Shared.Enum;
using Newtonsoft.Json.Linq;
namespace InterfaceForward.Application.Tests;
@@ -71,4 +76,57 @@ public class ScriptAssistantServiceTests
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);
+ }
+ }
}
\ No newline at end of file
diff --git a/方案B-重构清单-前后对比.md b/方案B-重构清单-前后对比.md
new file mode 100644
index 0000000..a543458
--- /dev/null
+++ b/方案B-重构清单-前后对比.md
@@ -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 Tab,KV | 真实 URL Query,不仅 FormUrlEncoded 才有 |
+| **Header** | `t_parameter_form`(FieldPosition=**Header**) | Header Tab,KV | 请求头;占位符 $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 Tab:Json 时常用 Raw;**接口配置**里 Json 仍用 **参数树** 定义结构(itfx 现有能力,且即映射体)。
+- ApiTest 的 FormUrlEncoded:URL 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` Normal(requestBody、上游 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 | 同 Json,Xml 序列化 |
+
+### 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 无 position;Json 也搞 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 Tab;Form 时 Mapping Body Tab;Json 时 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 全量 | Json:RequestBody 树;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)*