在大型企业级软件系统中,业务领域模型必须保持高度纯粹,绝不能直接依赖任何具体的第三方云服务 SDK(如电子签章服务、银行支付网关、全国企业工商查询 API 等)。直接耦合第三方 SDK 会导致系统难以进行本地单元测试、第三方厂商 API 变更直接破坏领域模型,并在第三方服务故障时引发全站雪崩。
BitzOrcas.Modern 严格贯彻六边形架构(Ports and Adapters,又称洋葱架构/整洁架构端口模式):
- 领域层定义窄端口(Port):仅声明业务所需的纯抽象接口,入参和返回值均为领域类型;
- 基础设施层实现适配器(Adapter):封装具体 HTTP 通讯、网络异常重试、签名算法与厂商 SDK;
- 提供 Fallback/Mock 回退实现:确保在离线开发、CI 自动化测试与第三方服务降级时系统平稳可用。
本篇指南将以**第三方电子合同云签章服务(如法大大/腾讯电子签)**为例,带你完整实现一个工业级基础设施适配器。
端口与适配器交互全景图
第一步:在领域层定义依赖倒置窄端口(Port)
在业务模块的 Domain 或 Contracts 层(例如 src/Modules/Legal/BitzOrcas.Modules.Legal.Domain/Ports/)定义强类型窄接口。
[!IMPORTANT] 端口设计铁律:
- 接口方法仅暴露业务所需的最小能力(窄端口原则);
- 入参与返回值严禁出现第三方 SDK 的专有类型,统一使用领域类型或
Result<T>;- 必须支持
CancellationToken取消令牌传播。
using System.Threading;using System.Threading.Tasks;using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Domain.Ports;
/// <summary>/// 电子签章第三方服务抽象端口/// </summary>/// <remarks>/// <para>业务领域层仅通过该窄端口与电子签章系统交互,与具体厂商 SDK 解耦。</para>/// <para>在生产环境中由真实云适配器实现,在本地测试环境中由 Mock 适配器回退。</para>/// </remarks>public interface IElectronicSignaturePort{ /// <summary> /// 异步发起合同多方在线签署流程 /// </summary> /// <param name="contractCode">合同业务流水号。</param> /// <param name="contractTitle">合同签署标题。</param> /// <param name="signerIdCard">签署人身份证件号码。</param> /// <param name="signerMobile">签署人接收验证码的手机号。</param> /// <param name="documentStorageUrl">待签署 PDF 合同文件在对象存储中的只读下载链接。</param> /// <param name="cancellationToken">异步取消令牌。</param> /// <returns>包含第三方签章任务唯一流水号的结果对象。</returns> Task<Result<string>> InitiateSignatureAsync( string contractCode, string contractTitle, string signerIdCard, string signerMobile, string documentStorageUrl, CancellationToken cancellationToken = default);
/// <summary> /// 异步查询指定签章任务的实时签署进度状态 /// </summary> /// <param name="signatureTaskId">第三方签章任务流水号。</param> /// <param name="cancellationToken">异步取消令牌。</param> /// <returns>当前签署状态(1=等待中, 2=已签署, 3=已拒签)。</returns> Task<Result<int>> QuerySignatureStatusAsync( string signatureTaskId, CancellationToken cancellationToken = default);}第二步:在基础设施层编写生产适配器(Adapter)
在 src/Platform/BitzOrcas.Infrastructure/Signature/ 中创建 FadadaSignatureAdapter.cs。
该适配器使用 HttpClient 调用第三方 API,并通过 Polly 实现网络抖动自动重试与错误封装:
using System;using System.Net.Http;using System.Net.Http.Json;using System.Text.Json;using System.Threading;using System.Threading.Tasks;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Domain.Ports;using Microsoft.Extensions.Logging;using Microsoft.Extensions.Options;
namespace BitzOrcas.Infrastructure.Signature;
/// <summary>/// 电子签章适配器稳定错误契约/// </summary>public static class SignatureErrors{ /// <summary> /// 第三方网关响应异常 /// </summary> public static readonly Error RemoteGatewayError = Error.Failure("Signature.RemoteGatewayError", "第三方签章通道响应异常。");
/// <summary> /// 第三方业务受理拒绝 /// </summary> public static readonly Error BusinessRejected = Error.Failure("Signature.BusinessRejected", "第三方签章接口业务受理失败。");
/// <summary> /// 签章通道网络通信超时 /// </summary> public static readonly Error NetworkTimeout = Error.Failure("Signature.NetworkTimeout", "签章通道通讯超时,请稍后重试。");
/// <summary> /// 签章任务流水未找到 /// </summary> public static readonly Error TaskNotFound = Error.NotFound("Signature.TaskNotFound", "未检索到指定签章任务流水。");
/// <summary> /// 查询签章进度失败 /// </summary> public static readonly Error QueryFailed = Error.Failure("Signature.QueryFailed", "查询签章进度失败。");}
/// <summary>/// 电子签章服务配置选项/// </summary>public sealed class FadadaSignatureOptions{ /// <summary> /// 配置节点名称 /// </summary> public const string SectionName = "Integrations:FadadaSignature";
/// <summary> /// 第三方网关基础地址 /// </summary> public string BaseUrl { get; set; } = "https://api.fadada.com/v2/";
/// <summary> /// 开发者应用凭证 AppId /// </summary> public string AppId { get; set; } = string.Empty;
/// <summary> /// 开发者签名私钥 AppSecret /// </summary> public string AppSecret { get; set; } = string.Empty;}
/// <summary>/// 第三方电子签章生产适配器/// </summary>/// <remarks>/// <para>实现 <see cref="IElectronicSignaturePort"/> 端口,承载与第三方云平台的 HTTP 通讯。</para>/// <para>负责请求参数加密、网络超时重试以及将第三方错误码映射为标准领域错误。</para>/// </remarks>public sealed class FadadaSignatureAdapter : IElectronicSignaturePort{ private readonly HttpClient _httpClient; private readonly FadadaSignatureOptions _options; private readonly ILogger<FadadaSignatureAdapter> _logger;
/// <summary> /// 初始化电子签章生产适配器 /// </summary> /// <param name="httpClient">弹性 HTTP 客户端。</param> /// <param name="options">配置选项。</param> /// <param name="logger">日志记录器。</param> public FadadaSignatureAdapter( HttpClient httpClient, IOptions<FadadaSignatureOptions> options, ILogger<FadadaSignatureAdapter> logger) { _httpClient = httpClient; _options = options.Value; _logger = logger; }
/// <summary> /// 异步发起合同签署流程 /// </summary> public async Task<Result<string>> InitiateSignatureAsync( string contractCode, string contractTitle, string signerIdCard, string signerMobile, string documentStorageUrl, CancellationToken cancellationToken = default) { try { // 1. 组装第三方特定的请求载荷 var payload = new { app_id = _options.AppId, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), contract_no = contractCode, title = contractTitle, doc_url = documentStorageUrl, signer = new { id_card = signerIdCard, mobile = signerMobile } };
// 2. 发起 HTTP POST 请求 var response = await _httpClient.PostAsJsonAsync("contract/initiate", payload, cancellationToken);
if (!response.IsSuccessStatusCode) { _logger.LogWarning("第三方签章服务响应 HTTP 异常状态码: {StatusCode}", response.StatusCode); return Result.Failure<string>( SignatureErrors.RemoteGatewayError.WithDescription($"第三方签章通道响应异常 (HTTP {response.StatusCode})。")); }
var responseData = await response.Content.ReadFromJsonAsync<FadadaApiResponse>(cancellationToken); if (responseData is null || responseData.Code != 1000) { _logger.LogWarning("第三方签章接口业务失败: Code={Code}, Msg={Msg}", responseData?.Code, responseData?.Message); return Result.Failure<string>( SignatureErrors.BusinessRejected.WithDescription(responseData?.Message ?? "第三方签章接口受理失败。")); }
// 3. 提取第三方签章流水号 return Result.Success(responseData.Data.TaskId); } catch (OperationCanceledException) { throw; // 允许取消令牌向上冒泡 } catch (Exception ex) { _logger.LogError(ex, "调用第三方电子签章服务发生未捕获网络异常: ContractCode={ContractCode}", contractCode); return Result.Failure<string>(SignatureErrors.NetworkTimeout); } }
/// <summary> /// 异步查询指定任务的签署状态 /// </summary> public async Task<Result<int>> QuerySignatureStatusAsync( string signatureTaskId, CancellationToken cancellationToken = default) { try { var response = await _httpClient.GetFromJsonAsync<FadadaApiResponse>( $"contract/status?task_id={Uri.EscapeDataString(signatureTaskId)}", cancellationToken);
if (response is null || response.Code != 1000) { return Result.Failure<int>(SignatureErrors.TaskNotFound); }
return Result.Success(response.Data.Status); } catch (Exception ex) { _logger.LogError(ex, "查询签章状态异常: TaskId={TaskId}", signatureTaskId); return Result.Failure<int>(SignatureErrors.QueryFailed); } }
private sealed class FadadaApiResponse { public int Code { get; set; } public string Message { get; set; } = string.Empty; public FadadaData Data { get; set; } = new(); }
private sealed class FadadaData { public string TaskId { get; set; } = string.Empty; public int Status { get; set; } }}第三步:编写本地与测试 Mock 适配器(Mock Adapter)
为了让开发同学在没有第三方外网 Key 的环境下也能顺畅本地调试,编写一个内存回退实现:
using System;using System.Threading;using System.Threading.Tasks;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Domain.Ports;using Microsoft.Extensions.Logging;
namespace BitzOrcas.Infrastructure.Signature;
/// <summary>/// 电子签章本地开发与单元测试 Mock 适配器/// </summary>/// <remarks>/// 仅在未配置第三方密钥或处于测试环境下自动生效,即时返回模拟签章流水号。/// </remarks>public sealed class MockSignatureAdapter : IElectronicSignaturePort{ private readonly ILogger<MockSignatureAdapter> _logger;
/// <summary> /// 初始化 Mock 适配器 /// </summary> public MockSignatureAdapter(ILogger<MockSignatureAdapter> logger) { _logger = logger; }
public Task<Result<string>> InitiateSignatureAsync( string contractCode, string contractTitle, string signerIdCard, string signerMobile, string documentStorageUrl, CancellationToken cancellationToken = default) { var mockTaskId = $"MOCK-SIG-{DateTime.UtcNow:yyyyMMdd}-{Random.Shared.Next(10000, 99999)}"; _logger.LogInformation("[MockSignatureAdapter] 已为合同【{ContractCode}】生成本地模拟签章流水: {TaskId}", contractCode, mockTaskId);
return Task.FromResult(Result.Success(mockTaskId)); }
public Task<Result<int>> QuerySignatureStatusAsync( string signatureTaskId, CancellationToken cancellationToken = default) { return Task.FromResult(Result.Success(2)); // 模拟已签署状态 }}第四步:使用 TryAdd 机制完成服务注册
在服务注册扩展方法中,使用 TryAddScoped 确保适配器可优雅回退:
using BitzOrcas.Modules.Legal.Domain.Ports;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.DependencyInjection.Extensions;
namespace BitzOrcas.Infrastructure.Signature;
/// <summary>/// 电子签章服务注册扩展/// </summary>public static class SignatureServiceCollectionExtensions{ /// <summary> /// 注册电子签章基础设施适配器 /// </summary> /// <param name="services">服务集合。</param> /// <param name="configuration">配置根。</param> /// <returns>链式调用服务集合。</returns> public static IServiceCollection AddElectronicSignatureAdapter( this IServiceCollection services, IConfiguration configuration) { var optionsSection = configuration.GetSection(FadadaSignatureOptions.SectionName); var options = optionsSection.Get<FadadaSignatureOptions>();
// 若配置了有效的 AppId 与 AppSecret,则注册生产适配器 if (options is not null && !string.IsNullOrWhiteSpace(options.AppId)) { services.Configure<FadadaSignatureOptions>(optionsSection); services.AddHttpClient<IElectronicSignaturePort, FadadaSignatureAdapter>(client => { client.BaseAddress = new Uri(options.BaseUrl); client.Timeout = TimeSpan.FromSeconds(15); }); } else { // 否则安全回退至本地 Mock 适配器,保证系统正常开机与测试运行 services.TryAddScoped<IElectronicSignaturePort, MockSignatureAdapter>(); }
return services; }}总结
六边形架构适配器模式为系统带来了核心价值:
- 领域纯粹性:业务 Handler 与聚合根只与抽象端口交互,完全免疫第三方厂商 API 变更;
- 高弹性保障:网络异常与 HTTP 状态码在适配器内部被标准化为
Result.Failure,绝不引发未处理异常崩溃; - 开箱即用开发体验:无密钥环境下自动激活 Mock 适配器,本地与 CI 流水线零配置秒级拉起。