Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Tutorial

新增厂商 Provider

以 Payment 域接入 PayPal 为例,9 步完成一家新厂商 Provider 的完整接入,从建项目到写测试。

Last updated

这篇教程用 Payment 域接入 PayPal 作样本,演示新增一家厂商 Provider 的完整流程。同样的 9 步适用于任何已有功能域(FileStorage、AI、eSign……)加新厂商。

接入全景

更新:Payment.All本次新增:Payment.PayPal已有:Bitzsoft.Integrations.Payment

IPaymentProvider

PaymentProviderCode

PayPalOptions

IPayPalConfigProvider

PayPalHttpClient

PayPalPaymentProvider

ServiceCollectionExtensions

PayPalProviderDescriptor

csproj + AddBitzsoftPayPalPayment

核心约束:抽象层已有契约不动,新增物全部落在 Bitzsoft.Integrations.Payment.PayPal 这个厂商包里,最后在聚合包 .All 的 csproj 和聚合注册方法里挂一个钩子。

准备:确认抽象层符号

开工前先确认目标域抽象包里这几个符号存在,新增厂商会反复引用它们:

符号位置用途
IPaymentProviderInterfaces/IPaymentProvider.cs厂商必须实现的能力接口
PaymentProviderCodeModels/PaymentProviderCode.cs厂商标识常量,这里要加 PayPal
PaymentResult<T> / PaymentResultPaymentResult.cs结果包装
PaymentExceptionPaymentException.cs领域异常
IPaymentConfigProvider<T>IPaymentConfigProvider.cs配置提供器基接口

抽象包还要通过 InternalsVisibleTo 向新厂商包开放 internal 成员:

<!-- src/Bitzsoft.Integrations.Payment/Bitzsoft.Integrations.Payment.csproj -->
<ItemGroup>
<InternalsVisibleTo Include="Bitzsoft.Integrations.Payment.Alipay" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Payment.WeChatPay" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Payment.Stripe" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Payment.PayPal" /> <!-- ① 新增 -->
<InternalsVisibleTo Include="Bitzsoft.Integrations.Payment.All" />
</ItemGroup>

第 1 步:创建项目结构

src/ 下新建 Bitzsoft.Integrations.Payment.PayPal,csproj 引用三个项目:Payment 抽象层、Compatibility(CompatibilityArgument 等跨 TFM 兼容垫片)、RequestLogging(HTTP 审计脱敏)。

<!-- src/Bitzsoft.Integrations.Payment.PayPal/Bitzsoft.Integrations.Payment.PayPal.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net8.0;net10.0</TargetFrameworks>
<PackageId>Bitzsoft.Integrations.Payment.PayPal</PackageId>
<Description>PayPal 支付实现 — 支持下单 / 查询 / 退款 / 回调验签</Description>
<PackageTags>paypal;payment;integration;Bitzsoft</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
<ItemGroup>
<!-- ① 抽象层 + 兼容垫片 + 请求日志,三件缺一不可 -->
<ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\Bitzsoft.Integrations.Compatibility.csproj" />
<ProjectReference Include="..\Bitzsoft.Integrations.Payment\Bitzsoft.Integrations.Payment.csproj" />
<ProjectReference Include="..\Bitzsoft.Integrations.RequestLogging\Bitzsoft.Integrations.RequestLogging.csproj" />
</ItemGroup>
</Project>

目录布局按既有约定:

Bitzsoft.Integrations.Payment.PayPal/
├── PayPalOptions.cs ← 厂商 Options(public sealed)
├── IPayPalConfigProvider.cs ← 配置提供器接口(internal)
├── PayPalConfigProvider.cs ← 默认实现(internal sealed)
├── PayPalPaymentProvider.cs ← Provider 实现(public sealed,ctor internal)
├── PayPalProviderDescriptor.cs ← Provider 描述(internal static Instance)
├── ServiceCollectionExtensions.cs ← DI 扩展(namespace Microsoft.Extensions.DependencyInjection)
├── GlobalUsings.cs
└── Internal/
└── PayPalHttpClient.cs ← HTTP 封装(internal sealed)

第 2 步:定义 Options

public sealed 类,字符串字段统一以 = string.Empty 兜底,避免 null。HttpClientName 默认 nameof(PayPalPaymentProvider)——这是命名 HttpClient 的关键约定,必须与 Provider 类名一致。

src/Bitzsoft.Integrations.Payment.PayPal/PayPalOptions.cs
namespace Bitzsoft.Integrations.Payment.PayPal;
/// <summary>PayPal 支付连接配置</summary>
public sealed class PayPalOptions
{
/// <summary>PayPal 应用 Client Id</summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>PayPal 应用 Secret</summary>
public string ClientSecret { get; set; } = string.Empty;
/// <summary>网关地址(正式 / 沙箱)</summary>
public string BaseUrl { get; set; } = "https://api-m.paypal.com";
/// <summary>异步通知 Webhook Id</summary>
public string? WebhookId { get; set; }
/// <summary>HTTP 请求超时时间</summary>
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>命名 HTTP 客户端标识,用于 IHttpClientFactory 匹配</summary>
public string HttpClientName { get; set; } = nameof(PayPalPaymentProvider); // ① 与 Provider 类名对齐
}

第 3 步:定义 ConfigProvider 接口

配置提供器抽象配置来源,支持 IOptions、数据库或配置中心。新厂商定义一个空接口继承 IPaymentConfigProvider<TOptions> 即可,标记为 internal——它是厂商包内部协作细节,不对外。

src/Bitzsoft.Integrations.Payment.PayPal/IPayPalConfigProvider.cs
namespace Bitzsoft.Integrations.Payment.PayPal;
/// <summary>PayPal 配置提供器接口</summary>
internal interface IPayPalConfigProvider : IPaymentConfigProvider<PayPalOptions>
{
}

默认实现从 IOptions<PayPalOptions> 取值,也标 internal sealed

src/Bitzsoft.Integrations.Payment.PayPal/PayPalConfigProvider.cs
using Microsoft.Extensions.Options;
namespace Bitzsoft.Integrations.Payment.PayPal;
/// <summary>PayPal 默认配置提供器</summary>
internal sealed class PayPalConfigProvider : IPayPalConfigProvider
{
private readonly IOptions<PayPalOptions> _options;
public PayPalConfigProvider(IOptions<PayPalOptions> options) => _options = options;
public PayPalOptions Current => _options.Value;
public string HttpClientName => _options.Value.HttpClientName;
}

第 4 步:实现 HttpClient 封装

把签名、请求构造、响应读取集中到一个 internal sealed 类,构造时用 IHttpClientFactory 拿命名客户端并设置超时。PayPal 用 OAuth2 换 access token,签名比支付宝简单——这是把厂商差异关在 Internal/ 里的意义。

src/Bitzsoft.Integrations.Payment.PayPal/Internal/PayPalHttpClient.cs
using System.Net.Http.Headers;
using System.Text.Json;
namespace Bitzsoft.Integrations.Payment.PayPal.Internal;
/// <summary>PayPal HTTP 客户端,封装 OAuth2 token 与 JSON 请求</summary>
internal sealed class PayPalHttpClient
{
private readonly HttpClient _httpClient;
private readonly IPayPalConfigProvider _config;
private string? _accessToken;
private DateTimeOffset _tokenExpiresAt;
public PayPalHttpClient(IHttpClientFactory httpClientFactory, IPayPalConfigProvider config)
{
var options = config.Current;
CompatibilityArgument.ThrowIfNullOrEmpty(options.ClientId);
CompatibilityArgument.ThrowIfNullOrEmpty(options.ClientSecret);
CompatibilityArgument.ThrowIfNullOrEmpty(options.BaseUrl);
_config = config;
_httpClient = httpClientFactory.CreateClient(config.HttpClientName); // ① 命名客户端
_httpClient.Timeout = options.Timeout;
}
public async Task<JsonElement> PostAsync(string path, object body, CancellationToken ct = default)
{
await EnsureTokenAsync(ct).ConfigureAwait(false);
using var content = JsonContent.Create(body);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
using var response = await _httpClient.PostAsync($"{_config.Current.BaseUrl}{path}", content, ct)
.ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
// ② 非 2xx 抛领域异常,由 Provider 统一捕获转 Result
throw new PaymentException(
PaymentProviderCode.PayPal,
$"HTTP_{(int)response.StatusCode}",
$"PayPal 请求失败: HTTP {(int)response.StatusCode}");
}
return string.IsNullOrEmpty(text) ? default : JsonDocument.Parse(text).RootElement.Clone();
}
private async Task EnsureTokenAsync(CancellationToken ct)
{
if (_accessToken is not null && DateTimeOffset.UtcNow < _tokenExpiresAt)
return;
// ③ client_credentials 换 token,缓存到过期前
using var form = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
});
// ... 用 Basic auth 换 token,略
_accessToken = "EC-...";
_tokenExpiresAt = DateTimeOffset.UtcNow.AddMinutes(45);
}
}

第 5 步:实现 Provider

public sealed class 实现 IPaymentProvider,构造函数标 internal(禁止手动 new,只走 DI)。每个方法按 catch (PaymentException)catch (HttpRequestException) 分类型捕获,统一转成 PaymentResult

下面以查询状态为例,展示标准三段式:构造业务参数 → 调 HttpClient → 捕获两类异常转 Result。

src/Bitzsoft.Integrations.Payment.PayPal/PayPalPaymentProvider.cs
using System.Text.Json;
using Bitzsoft.Integrations.Payment.PayPal.Internal;
using Microsoft.Extensions.Logging;
namespace Bitzsoft.Integrations.Payment.PayPal;
/// <summary>PayPal 支付实现</summary>
public sealed class PayPalPaymentProvider : IPaymentProvider
{
private readonly PayPalHttpClient _httpClient;
private readonly IPayPalConfigProvider _config;
private readonly ILogger<PayPalPaymentProvider> _logger;
internal PayPalPaymentProvider( // ① internal ctor,禁止外部 new
PayPalHttpClient httpClient,
IPayPalConfigProvider config,
ILogger<PayPalPaymentProvider> logger)
{
_httpClient = httpClient;
_config = config;
_logger = logger;
}
public string Code => PaymentProviderCode.PayPal; // ② 复用抽象层常量
public async Task<PaymentResult<PaymentStatusResult>> QueryStatusAsync(
string outTradeNo, CancellationToken ct = default)
{
CompatibilityArgument.ThrowIfNullOrEmpty(outTradeNo);
_logger.LogInformation("PayPal 查询订单状态: {OutTradeNo}", outTradeNo);
JsonElement json;
try
{
json = await _httpClient.PostAsync("/v2/checkout/orders", new { }, ct).ConfigureAwait(false);
}
catch (PaymentException ex) // ③ 先捕获领域异常,保留原始错误码
{
_logger.LogWarning("PayPal 查询订单失败: {Code} - {Msg}", ex.ErrorCode, ex.ProviderMessage);
return PaymentResult<PaymentStatusResult>.Fail(ex.ErrorCode ?? "QUERY_FAILED", ex.ProviderMessage ?? ex.Message);
}
catch (HttpRequestException ex) // ④ 再捕获网络异常,归一为 HTTP_ERROR
{
_logger.LogWarning(ex, "PayPal 查询订单网络异常");
return PaymentResult<PaymentStatusResult>.Fail("HTTP_ERROR", ex.Message);
}
var statusResult = new PaymentStatusResult
{
OutTradeNo = outTradeNo,
TradeNo = json.GetProperty("id").GetString(),
Status = MapStatus(json.GetProperty("status").GetString()),
// ... 其余字段映射
};
return PaymentResult<PaymentStatusResult>.Success(statusResult);
}
// CreateOrderAsync / CloseOrderAsync / RefundAsync / VerifyCallbackAsync 同结构,略
private static PaymentStatus MapStatus(string? s) => s switch
{
"COMPLETED" => PaymentStatus.Success,
"APPROVED" => PaymentStatus.Pending,
_ => PaymentStatus.Pending,
};
}

异常处理顺序很重要

网络层失败

调用 HttpClient

2xx?

抛 PaymentException
保留厂商原始错误码

解析响应体

catch PaymentException
→ Result.Fail(原始码)

抛 HttpRequestException

catch HttpRequestException
→ Result.Fail(HTTP_ERROR)

业务码成功?

Result.Fail(业务码)

Result.Success(data)

PaymentException 必须在 HttpRequestException 之前 catch——PaymentException 继承自 IntegrationException,携带厂商原始错误码,是更具体的信息;HttpRequestException 是兜底的网络层归一。

第 6 步:写 DI 扩展

扩展类放在 Microsoft.Extensions.DependencyInjection 命名空间(这样消费方 using 一次就能用),方法命名 AddBitzsoft{厂商}{域}()。提供「委托配置」和「IConfiguration 配置」两个重载。

src/Bitzsoft.Integrations.Payment.PayPal/ServiceCollectionExtensions.cs
using Bitzsoft.Integrations.Payment.PayPal;
using Bitzsoft.Integrations.Payment.PayPal.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.DependencyInjection; // ① 关键:放这个命名空间
/// <summary>PayPal 支付服务注册扩展</summary>
public static class PayPalServiceCollectionExtensions
{
/// <summary>从委托注册 PayPal 支付服务</summary>
public static IServiceCollection AddBitzsoftPayPalPayment(
this IServiceCollection services, Action<PayPalOptions> configure)
{
CompatibilityArgument.ThrowIfNull(configure);
var options = new PayPalOptions();
configure(options);
services.AddHttpClient(options.HttpClientName).AddRequestLogging(options.HttpClientName); // ② 挂审计日志
services.Configure(configure);
services.TryAddSingleton<IPayPalConfigProvider, PayPalConfigProvider>();
RegisterProvider(services);
return services;
}
/// <summary>从 IConfiguration 注册 PayPal 支付服务</summary>
public static IServiceCollection AddBitzsoftPayPalPayment(
this IServiceCollection services, IConfiguration configuration,
string sectionKey = "PayPal")
{
CompatibilityArgument.ThrowIfNull(configuration);
var options = new PayPalOptions();
configuration.GetSection(sectionKey).Bind(options);
services.AddHttpClient(options.HttpClientName).AddRequestLogging(options.HttpClientName);
services.Configure<PayPalOptions>(configuration.GetSection(sectionKey));
services.TryAddSingleton<IPayPalConfigProvider, PayPalConfigProvider>();
RegisterProvider(services);
return services;
}
private static void RegisterProvider(IServiceCollection services)
{
services.TryAddTransient<PayPalHttpClient>();
services.AddIntegrationProviderCapability<IPaymentProvider, PayPalPaymentProvider>( // ③ 提交 Descriptor + 实现
PayPalProviderDescriptor.Instance,
sp => new PayPalPaymentProvider(
sp.GetRequiredService<PayPalHttpClient>(),
sp.GetRequiredService<IPayPalConfigProvider>(),
sp.GetRequiredService<ILogger<PayPalPaymentProvider>>()));
}
}

每个重载内部做四件事:注册命名 HttpClient 并挂 AddRequestLogging、绑定 Options、注册 IConfigProvider、调用 AddIntegrationProviderCapability 把实现和 Descriptor 提交到目录。

第 7 步:定义 Provider Descriptor

internal static 类,暴露一个 IntegrationProviderDescriptor.Instance。这是 Provider 的事实源描述,Catalog 和 Resolver 都依赖它。

src/Bitzsoft.Integrations.Payment.PayPal/PayPalProviderDescriptor.cs
namespace Bitzsoft.Integrations.Payment.PayPal;
internal static class PayPalProviderDescriptor
{
public static readonly IntegrationProviderDescriptor Instance = new(
providerId: PaymentProviderCode.PayPal, // ① 与抽象层常量一致
displayName: "PayPal",
domain: "Payment",
capabilityTypes: new[] { typeof(IPaymentProvider) },
stability: IntegrationProviderStability.Preview, // ② 新接入先标 Preview,稳定后升 Stable
vendor: "PayPal",
apiVersion: "v2",
authenticationType: "OAuth2",
regions: new[] { "Global" },
environments: new[] { "Production", "Sandbox" });
}

providerId 必须等于第 2 步抽象层里加的那个常量,否则 Resolver 按字符串解析时会失配。stability 新接入一律先标 Preview,走通全部场景再升 Stable

第 8 步:更新聚合包

聚合包 .All 改两处:csproj 加 ProjectReference,聚合注册方法加一行条件注册。

<!-- src/Bitzsoft.Integrations.Payment.All/Bitzsoft.Integrations.Payment.All.csproj -->
<ItemGroup>
<ProjectReference Include="..\Bitzsoft.Integrations.Payment.Alipay\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Payment.WeChatPay\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Payment.Stripe\..." />
<ProjectReference Include="..\Bitzsoft.Integrations.Payment.PayPal\Bitzsoft.Integrations.Payment.PayPal.csproj" /> <!-- ① 新增引用 -->
</ItemGroup>

聚合注册方法按配置节存在性自动注册——不存在的厂商不会被注册:

src/Bitzsoft.Integrations.Payment.All/ServiceCollectionExtensions.cs
public static IServiceCollection AddBitzsoftPaymentAll(
this IServiceCollection services, IConfiguration configuration,
string sectionKey = "Payment")
{
var section = configuration.GetSection(sectionKey);
if (section.GetSection("Alipay").Exists())
services.AddBitzsoftAlipayPayment(configuration, $"{sectionKey}:Alipay");
if (section.GetSection("WeChatPay").Exists())
services.AddBitzsoftWeChatPayPayment(configuration, $"{sectionKey}:WeChatPay");
if (section.GetSection("Stripe").Exists())
services.AddBitzsoftStripePayment(configuration, $"{sectionKey}:Stripe");
if (section.GetSection("PayPal").Exists()) // ② 新增条件注册
services.AddBitzsoftPayPalPayment(configuration, $"{sectionKey}:PayPal");
return services;
}

第 9 步:写 README 与测试

包 README

简明列出能力、配置项、稳定性。例如:

# Bitzsoft.Integrations.Payment.PayPal
PayPal 支付实现,遵循 Bitzsoft.Integrations 三层包模式。
## 能力
- 创建订单(Capture order)
- 查询状态 / 关闭订单 / 退款
- Webhook 回调验签
## 配置(appsettings.json)
\`\`\`json
{ "PayPal": { "ClientId": "...", "ClientSecret": "...", "BaseUrl": "https://api-m.sandbox.paypal.com" } }
\`\`\`
## 注册
\`\`\`csharp
builder.Services.AddBitzsoftPayPalPayment(builder.Configuration.GetSection("PayPal"));
\`\`\`
## 稳定性
Preview — 主流程已验证,边缘场景持续补充。

单元测试

至少覆盖:可正常解析、业务错误码透传、网络异常归一为 HTTP_ERROR。参考既有 ProviderRouting.Tests 的写法:

tests/Bitzsoft.Integrations.Payment.PayPal.Tests
[Fact]
public async Task QueryStatus_PaypalBusinessError_PreservesErrorCode()
{
// ① 构造一个返回 HTTP 400 的 HttpMessageHandler
var handler = new StubHandler(System.Net.HttpStatusCode.BadRequest, "{}");
var services = new ServiceCollection();
services.AddHttpClient(nameof(PayPalPaymentProvider)).AddRequestLogging(nameof(PayPalPaymentProvider))
.ConfigurePrimaryHttpMessageHandler(() => handler);
services.AddBitzsoftPayPalPayment(_ => _.ClientId = "c"); // 其余配置略
var sp = services.BuildServiceProvider();
var provider = sp.GetRequiredService<IPaymentProvider>();
var result = await provider.QueryStatusAsync("ORDER-1");
Assert.False(result.IsSuccess);
Assert.StartsWith("HTTP_", result.ErrorCode); // ② 厂商错误码透传
}
[Fact]
public void Payment_All_Registers_PayPal_When_Configured()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Payment:PayPal:ClientId"] = "test",
})
.Build();
var services = new ServiceCollection();
services.AddBitzsoftPaymentAll(config); // ③ 聚合包注册
var sp = services.BuildServiceProvider(validateScopes: true);
var resolver = sp.GetRequiredService<IIntegrationProviderResolver<IPaymentProvider>>();
Assert.True(resolver.TryResolve("PayPal", out _)); // ④ 可按 ID 解析
}

完整清单

接入完成前逐项核对:

  • 抽象层:PaymentProviderCode 加了厂商常量
  • 抽象层:InternalsVisibleTo 加了新厂商包
  • 厂商包:csproj 引用 Payment + Compatibility + RequestLogging
  • 厂商包:Options 字符串字段全部 = string.EmptyHttpClientName = nameof(Provider)
  • 厂商包:IConfigProvider 接口 internal,默认实现 internal sealed
  • 厂商包:HttpClient 封装在 Internal/,构造时 CreateClient(config.HttpClientName)
  • 厂商包:Providerpublic sealed,ctor internalcatch PaymentException 先于 catch HttpRequestException
  • 厂商包:DI 扩展在 Microsoft.Extensions.DependencyInjection 命名空间,方法名 AddBitzsoft{厂商}{域}()
  • 厂商包:ProviderDescriptor.InstanceproviderId 与抽象层常量一致,stability 先标 Preview
  • 聚合包:csproj 加 ProjectReference,AddBitzsoft{域}All() 加条件注册
  • 测试:业务错误码透传 + 网络异常归一 + 聚合包可解析,三项齐备
  • README:能力、配置、稳定性标注完整

下一步

100%

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