问题
在一些集成系统中,我们通常会需要对接多个外部Http接口,较为传统的做法是:写一个 Service,定义一套 Request/Response 实体,然后封装 HttpClient。但是这种情况下,不禁要思考:接口比较多时,代码较为冗余,业务逻辑被淹没在协议拼装中。为解决此问题:分享一套插件化Http协议发送引擎。核心思路:将协议定义与发送逻辑剥离,利用 Scriban 模板引擎实现动态适配。
一、核心设计
目标:新增一个协议,只需要新增一个插件,无需修改引擎代码。设计分为三层:
1.数据抽象层:获得原始对象BusinessData
2.协议插件层:将原始数据转换为模版所需要变量池
3.执行引擎层:渲染模版并执行最终Http请求
二、核心接口定义:IHttpPlugin
cs
/// <summary>
/// HTTP 协议插件抽象接口
/// </summary>
public interface IHttpPlugin
{
// 插件唯一标识,用于引擎索引
string PluginId { get; }
// 协议元数据(如 URL、Method、超时时间等)
ProtocolMetadata Metadata { get; }
/// <summary>
/// 核心转换逻辑:将业务数据映射为模板变量池
/// </summary>
Task<IDictionary<string, object>> MapToContextAsync(object businessData);
/// <summary>
/// 针对不同协议的特殊处理(如动态签名、特殊加密等)
/// </summary>
Task SpecialContextAsync(HttpRequestMessage request, object data, string rawJson);
}
设计思路:
MapToContextAsync:解决字段名不一致问题
SpecialContextAsync:给复杂场景留了"后门"。比如某些接口需要Aes加密等等
三、 Scriban 模板引擎
"Scriban"可能是指一种模板引擎,它主要用于.NET平台,用来生成文本输出,如HTML、电子邮件等。
为什么选择 Scriban?
性能高(Scriban 的解析速度快)
逻辑支持(支持if/else,for循环)
cs
{
"bCode": "{{ b_code }}",
"eDataList": [
{% for item in items %}
{
"eNo": "{{ item.no }}",
"xPhotos": [ {{ item.photos | array.join ',' }} ]
}
{% endfor %}
]
}
四、 自动化发送引擎
自动发送引擎流程:找到插件 -> 准备上下文 -> 渲染模板 -> 发送。
cs
public async Task<PResponse> SendThirdPartyAsync(string pluginId, object businessData)
{
// 1. 获取对应插件
var plugin = _provider.GetPlugin(pluginId);
// 1. 获取平铺后的数据上下文
var context = await plugin.MapToCpontextAsync(businessData);
// 2. 使用 Scriban 渲染模板
// Scriban 能够处理循环 (for)、条件 (if) 以及复杂的嵌套对象
var template = Scriban.Template.Parse(plugin.Metadata.BodyTemplate);
string finalJson = await template.RenderAsync(context);
// 3. 构建 HTTP 请求
var request = new HttpRequestMessage(new HttpMethod(plugin.Metadata.Method), plugin.Metadata.Url)
{
Content = new StringContent(finalJson, Encoding.UTF8, "application/json")
};
//plugin.Metadata.Headers = await plugin.MapToCpontextAsync(businessData);
// 4.注入 Header
foreach (var header in plugin.Metadata.Headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
//调用插件特殊化处理
await plugin.SpecialContextAsync(request, businessData,finalJson);
var response = new PResponse();
// 5. 发送并返回结果
var result = await _httpClient.SendAsync(request);
//读取响应内容
var responseJson = await result.Content.ReadAsStringAsync();
response = JsonConvert.DeserializeObject<PResponse>(responseJson)!;
if (response.Code != 200)
{
response.SetResponseCode((int)result.StatusCode, $"上传失败(状态码:{result.StatusCode})");
return response;
}
return response;
}
五、实现一个HTTP接口插件
cs
public class ImagesPlugin : IHttpPlugin
{
public string PluginId => "ERP_ORDER_SYNC";
public ProtocolMetadata Metadata => new ProtocolMetadata
{
UrlTemplate = "http://api.erp.com/v1/save",
Method = "POST",
Headers = new Dictionary<string, string> { { "Auth-Token", "xadasd" } },
// 这里的模板支持循环处理 eDataList
BodyTemplate = @"
{
""business_order_id"": ""{{ business_order_id}}"",
""DataList"": [
{% for item in items %}
{
""eNo"": ""{{ item.no }}"",
""sName"": ""{{ item.name }}"",
""Images "": [
{% for img in item.Images %} ""{{ img }}"" {{ if !for.last }},{{ end }} {% endfor %}
]
}{{ if !for.last }},{{ end }}
{% endfor %}
]
}"
};
public async Task<IDictionary<string, object>> MapToContextAsync(object data)
{
var order = data as OrderEntity;
return new Dictionary<string, object>
{
{ "business_order_id", order.Id },
{ "amount", order.TotalAmount }
{ "items", task.SubItems.Select(s => new {
no = s.Id,
Images = s.CapturedImages
}
};
}
public Task PrepareRequestAsync(HttpRequestMessage request, object data, string rawJson)
{
//1.处理加密 Body
string encryptedBody = EncryptHelper.AESEncrypt(rawJson);
request.Content = new StringContent(encryptedBody, Encoding.UTF8, "application/json");
// 2. 处理特殊 Header
var info = (SecurityImageCloudDataDto)data;
....
....
request.Headers.Add("Auth", $"{authEncrypted}");
request.Headers.Add("User-Agent", $"{info.MachineNumber}");
return Task.CompletedTask;
}
}
基于上述设计,我们实现了业务代码与Http协议解耦,复杂的报文转换交由 Scriban 模板,较为灵活。以此扩展多个不同http协议将变得更简单。