Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Tutorial

创建全新的域

以 Captcha 验证码域为例,从抽象包、厂商包到聚合包,搭起一个全新功能域的完整流程。

Last updated

「新增厂商」是在已有域里加一家供应商;「创建全新的域」是从零搭起一个能力领域。这篇教程用 Captcha(验证码) 域做样本,演示如何定义领域契约、接入第一家厂商、再做一个聚合包收尾。

库里的 Captcha 域已经存在,本教程复用它的真实结构作示例素材,便于你对照源码学习。

域的全貌

一个新域由三个包组成,遵循三层包模式

Bitzsoft.Integrations.Captcha.AllBitzsoft.Integrations.Captcha.AliyunBitzsoft.Integrations.Captcha

ICaptchaProvider 接口

DTO / 枚举

CaptchaResult T

CaptchaException

CaptchaProviderCode

AliyunOptions

AliyunCaptchaProvider

ServiceCollectionExtensions

AddBitzsoftCaptchaAll

抽象包定义「这个域干什么」,厂商包定义「某家具体怎么做」,聚合包一键拉起所有厂商。

第 1 步:创建抽象包

包名 Bitzsoft.Integrations.{域}(如 Bitzsoft.Integrations.Captcha),引用 Core 和 Compatibility 两个基础包。抽象包不含任何厂商逻辑,也不做 DI 注册。

<!-- src/Bitzsoft.Integrations.Captcha/Bitzsoft.Integrations.Captcha.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net8.0;net10.0</TargetFrameworks>
<PackageId>Bitzsoft.Integrations.Captcha</PackageId>
<Description>验证码抽象层 — 统一接口定义与基础模型</Description>
<PackageTags>captcha;abstraction;integration;Bitzsoft</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Bitzsoft.Integrations.Core\Bitzsoft.Integrations.Core.csproj" />
<ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\Bitzsoft.Integrations.Compatibility.csproj" />
</ItemGroup>
<ItemGroup>
<!-- ① 向未来的厂商包和聚合包开放 internal 成员 -->
<InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.Aliyun" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.Tencent" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.GeeTest" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.All" />
</ItemGroup>
</Project>

抽象包要定义五类成员,缺一不可。

1.1 领域接口

参照 Payment 域的 IPaymentProvider,定义域的能力契约。方法签名都要返回 CaptchaResult<T>,不要抛异常给调用方。

src/Bitzsoft.Integrations.Captcha/Interfaces/ICaptchaProvider.cs
using Bitzsoft.Integrations.Captcha.Models;
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码统一抽象接口</summary>
/// <remarks>
/// 封装无感验证码的「生成挑战」与「服务端二次校验」能力。
/// 前端 SDK 自渲染挑战 → 用户无感验证 → 前端提交 token → 后端调校验 API。
/// </remarks>
public interface ICaptchaProvider
{
/// <summary>供应商编码</summary>
string Code { get; }
/// <summary>生成验证码挑战,返回前端 SDK 初始化所需凭证</summary>
Task<CaptchaResult<CaptchaChallenge>> GenerateAsync(
CaptchaGenerateRequest request, CancellationToken ct = default);
/// <summary>服务端二次校验前端提交的 token</summary>
Task<CaptchaResult<CaptchaVerification>> VerifyAsync(
CaptchaVerifyRequest request, CancellationToken ct = default);
}

1.2 配置提供器基接口

抽象包定义一个泛型基接口,厂商包各自继承。HttpClientName 让厂商实现能拿到命名客户端。

src/Bitzsoft.Integrations.Captcha/Interfaces/ICaptchaConfigProvider.cs
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码配置提供器接口</summary>
/// <typeparam name="TOptions">供应商配置选项类型</typeparam>
public interface ICaptchaConfigProvider<TOptions> where TOptions : class
{
/// <summary>当前供应商配置选项</summary>
TOptions Current { get; }
/// <summary>HttpClient 注册名称,用于 IHttpClientFactory 创建命名客户端</summary>
string HttpClientName { get; }
}

1.3 结果类型

仿照 PaymentResult<T>,定义带数据和不带数据两种。提供 Success / Fail 工厂方法。

src/Bitzsoft.Integrations.Captcha/CaptchaResult.cs
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码结果包装</summary>
public sealed class CaptchaResult<T>
{
public bool IsSuccess { get; init; }
public T? Data { get; init; }
public string? ErrorCode { get; init; }
public string? ErrorMessage { get; init; }
public static CaptchaResult<T> Success(T data) => new() { IsSuccess = true, Data = data };
public static CaptchaResult<T> Fail(string errorCode, string errorMessage) =>
new() { IsSuccess = false, ErrorCode = errorCode, ErrorMessage = errorMessage };
}
/// <summary>无数据结果,仅表示成功 / 失败</summary>
public sealed class CaptchaResult
{
public bool IsSuccess { get; init; }
public string? ErrorCode { get; init; }
public string? ErrorMessage { get; init; }
public static CaptchaResult Success() => new() { IsSuccess = true };
public static CaptchaResult Fail(string errorCode, string errorMessage) =>
new() { IsSuccess = false, ErrorCode = errorCode, ErrorMessage = errorMessage };
}

1.4 领域异常

继承 IntegrationException,构造时传 domain 标识和厂商原始错误码。HttpClient 封装遇到非 2xx 抛这个异常,Provider 捕获后转 CaptchaResult

src/Bitzsoft.Integrations.Captcha/CaptchaException.cs
using Bitzsoft.Integrations.Core;
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码统一异常</summary>
public class CaptchaException : IntegrationException
{
public string ProviderName => Provider ?? string.Empty;
public new string? ErrorCode => base.ErrorCode;
public new string? ProviderMessage => base.ProviderMessage;
public CaptchaException(string providerName, string? errorCode, string? providerMessage)
: base(
domain: "Captcha", // ① 域标识,与目录里的 domain 字段一致
message: $"[{providerName}] {errorCode}: {providerMessage}",
provider: providerName,
errorCode: errorCode,
providerMessage: providerMessage)
{
}
}

1.5 厂商标识常量

这是 Provider ID 的事实源。每接入一家厂商在这里加一个常量,发布后不可改名。

src/Bitzsoft.Integrations.Captcha/Models/CaptchaProviderCode.cs
namespace Bitzsoft.Integrations.Captcha.Models;
/// <summary>验证码供应商标识</summary>
public static class CaptchaProviderCode
{
public const string Aliyun = "Aliyun"; // 阿里云验证码 2.0
public const string Tencent = "Tencent"; // 腾讯天御
public const string GeeTest = "GeeTest"; // 极验
}

第 2 步:创建第一家厂商包

流程和新增厂商 Provider 指南完全一致——这里只列关键骨架,完整 9 步参考那篇指南。

包名 Bitzsoft.Integrations.{域}.{厂商}(如 Bitzsoft.Integrations.Captcha.Aliyun),引用抽象包 + Compatibility + RequestLogging:

<!-- src/Bitzsoft.Integrations.Captcha.Aliyun/Bitzsoft.Integrations.Captcha.Aliyun.csproj -->
<ItemGroup>
<ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Captcha\Bitzsoft.Integrations.Captcha.csproj" />
<ProjectReference Include="..\Bitzsoft.Integrations.RequestLogging\..." />
</ItemGroup>

厂商包关键文件:

// Options:字符串字段 = string.Empty,HttpClientName = nameof(Provider)
public sealed class AliyunOptions
{
public string AccessKeyId { get; set; } = string.Empty;
public string AccessKeySecret { get; set; } = string.Empty;
public string SceneId { get; set; } = string.Empty;
public string Endpoint { get; set; } = "captcha.aliyuncs.com";
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
public string HttpClientName { get; set; } = nameof(AliyunCaptchaProvider); // ① 对齐类名
}
// Provider:public sealed,ctor internal,异常处理 PaymentException → HttpRequestException
public sealed class AliyunCaptchaProvider : ICaptchaProvider
{
public string Code => CaptchaProviderCode.Aliyun;
public async Task<CaptchaResult<CaptchaVerification>> VerifyAsync(
CaptchaVerifyRequest request, CancellationToken ct = default)
{
try
{
var json = await _httpClient.PostAsync("/verify", request, ct);
// ... 解析
return CaptchaResult<CaptchaVerification>.Success(new CaptchaVerification { Passed = true });
}
catch (CaptchaException ex) // ② 先领域异常
{
return CaptchaResult<CaptchaVerification>.Fail(ex.ErrorCode ?? "VERIFY_FAILED", ex.ProviderMessage ?? ex.Message);
}
catch (HttpRequestException ex) // ③ 后网络异常
{
return CaptchaResult<CaptchaVerification>.Fail("HTTP_ERROR", ex.Message);
}
}
}
// DI 扩展:namespace Microsoft.Extensions.DependencyInjection,方法名 AddBitzsoftAliyunCaptcha
public static class AliyunCaptchaServiceCollectionExtensions
{
public static IServiceCollection AddBitzsoftAliyunCaptcha(
this IServiceCollection services, IConfiguration configuration,
string sectionKey = "Captcha:Aliyun")
{
// ... AddHttpClient(name).AddRequestLogging(name)
// ... AddIntegrationProviderCapability<ICaptchaProvider, AliyunCaptchaProvider>(descriptor, factory)
}
}
// Descriptor:providerId 与抽象层常量一致
internal static class AliyunCaptchaProviderDescriptor
{
public static readonly IntegrationProviderDescriptor Instance = new(
providerId: CaptchaProviderCode.Aliyun,
displayName: "阿里云验证码",
domain: "Captcha",
capabilityTypes: new[] { typeof(ICaptchaProvider) },
stability: IntegrationProviderStability.Preview,
vendor: "Alibaba Cloud",
apiVersion: "2.0",
authenticationType: "AK/SK",
regions: new[] { "CN" },
environments: new[] { "Production" });
}

第 3 步:创建聚合包

包名 Bitzsoft.Integrations.{域}.All(如 Bitzsoft.Integrations.Captcha.All)。聚合包有两个反直觉的设定:IncludeBuildOutput=false(自身几乎不产 IL),并用 _._ 占位文件填进各 TFM 的 lib 目录。

<!-- src/Bitzsoft.Integrations.Captcha.All/Bitzsoft.Integrations.Captcha.All.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net8.0;net10.0</TargetFrameworks>
<PackageId>Bitzsoft.Integrations.Captcha.All</PackageId>
<Description>验证码聚合包 — 包含阿里云 / 腾讯 / 极验全部实现</Description>
<PackageTags>captcha;all;integration;Bitzsoft</PackageTags>
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- ① 不产 IL -->
<IncludeSymbols>false</IncludeSymbols>
</PropertyGroup>
<ItemGroup>
<!-- ② _._ 占位文件放进各 TFM 的 lib 目录,避免 NuGet "空 lib" 警告 -->
<None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net5.0\_._" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net8.0\_._" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net10.0\_._" Visible="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
</ItemGroup>
<ItemGroup>
<!-- ③ ProjectReference 把所有厂商拉进来 -->
<ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Captcha\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Captcha.Aliyun\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Captcha.Tencent\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Captcha.GeeTest\..." />
</ItemGroup>
</Project>

聚合包唯一的编译产物是一个配置驱动的注册方法:消费者只装一个 .All 包、写一段配置、调一个方法,就能按配置节的存在性自动注册对应厂商。

src/Bitzsoft.Integrations.Captcha.All/ServiceCollectionExtensions.cs
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection;
public static class CaptchaServiceCollectionExtensions
{
/// <summary>注册全部验证码供应商(仅注册配置中存在的)</summary>
public static IServiceCollection AddBitzsoftCaptchaAll(
this IServiceCollection services, IConfiguration configuration,
string sectionKey = "Captcha")
{
var section = configuration.GetSection(sectionKey);
// ① 按配置节存在性自动注册——不存在的厂商不会被注册
if (section.GetSection("Aliyun").Exists())
services.AddBitzsoftAliyunCaptcha(configuration, $"{sectionKey}:Aliyun");
if (section.GetSection("Tencent").Exists())
services.AddBitzsoftTencentCaptcha(configuration, $"{sectionKey}:Tencent");
if (section.GetSection("GeeTest").Exists())
services.AddBitzsoftGeeTestCaptcha(configuration, $"{sectionKey}:GeeTest");
return services;
}
}

为什么用 IncludeBuildOutput=false + _._

编译ProjectReferenceNuGet 打包 _._ 占位实际产物

Captcha.All 项目

几乎无 IL

拉入各厂商包

lib/net8.0/

避免空 lib 警告

AddBitzsoftCaptchaAll 方法

聚合包的价值是「集合 + 一键注册」,不是它自己的代码量。_._ 是 NuGet 约定的占位文件,让包结构合规。

第 4 步:更新根 README 包索引

在仓库根 README.md 的包索引表里,把这个新域加进去,并标注稳定性。这是贡献者发现新域的入口。

## 验证码(Captcha)
| 包 | 说明 | 稳定性 |
| --- | --- | --- |
| Bitzsoft.Integrations.Captcha | 验证码抽象层 | Stable |
| Bitzsoft.Integrations.Captcha.Aliyun | 阿里云验证码 2.0 | Preview |
| Bitzsoft.Integrations.Captcha.Tencent | 腾讯天御 | Preview |
| Bitzsoft.Integrations.Captcha.GeeTest | 极验 GeeTest | Preview |
| Bitzsoft.Integrations.Captcha.All | 聚合包 | Preview |

完整清单

步骤产物关键校验点
抽象包{域} 接口 / DTO / Result / Exception / ProviderCode / ConfigProvider 接口 / InternalsVisibleTo不含 Options、不做 DI 注册
厂商包Options / HttpClient / Provider / DI 扩展 / DescriptorHttpClientName=nameof(Provider),异常处理顺序正确
聚合包.All csproj + AddBitzsoft{域}All()IncludeBuildOutput=false_._ 占位、配置驱动注册
文档根 README 包索引抽象层 Stable,厂商 Preview

下一步

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%