基于插件化 + Scriban 模板引擎的高效 HTTP 协议中心设计

问题

在一些集成系统中,我们通常会需要对接多个外部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协议将变得更简单。

相关推荐
Remember_9932 小时前
MySQL 索引详解:从原理到实战优化
java·数据库·mysql·spring·http·adb·面试
青云计划5 小时前
知光项目用户关系模块
c#·linq
m5655bj5 小时前
使用 C# 修改 PDF 页面尺寸
java·pdf·c#
专注VB编程开发20年5 小时前
c#模仿内置 Socket.Receive(无需 out/ref,直接写回数据)
开发语言·c#
bugcome_com6 小时前
【零基础入门】C# 核心教程:从 HelloWorld 到入门精髓
c#
Zach_yuan6 小时前
从零理解 HTTP:协议原理、URL 结构与简易服务器实现
linux·服务器·网络协议·http
JQLvopkk6 小时前
C# 实现Http Json格式 Post 、Get 方法请求 winform服务器
http·c#·json
JQLvopkk6 小时前
C# 实践AI 编码:Visual Studio + VSCode 组合方案
人工智能·c#·visual studio
暖馒6 小时前
深度剖析串口通讯(232/485)
开发语言·c#·wpf·智能硬件