引入微服务通用通讯客户端及标准化响应
新增 Microservice.Common 通用库,基于 Nacos 和 Refit 实现 IServiceClient,统一微服务间调用方式和 API 响应结构。Demo 与 Demo2 服务集成 ServiceClient,接口返回结构标准化。新增单元测试项目,完善服务间通讯的可测试性。调整依赖与日志配置,提升项目可维护性和扩展性。
This commit is contained in:
parent
590d5ab936
commit
cf3ad910c2
|
|
@ -1,4 +1,6 @@
|
||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="MicoService.Demo/MicoService.Demo.csproj" />
|
<Project Path="MicoService.Demo/MicoService.Demo.csproj" />
|
||||||
<Project Path="MicoService.Demo2/MicoService.Demo2.csproj" Id="efd7de63-eb5d-4687-8e58-967bc9d939e1" />
|
<Project Path="MicoService.Demo2/MicoService.Demo2.csproj" Id="efd7de63-eb5d-4687-8e58-967bc9d939e1" />
|
||||||
|
<Project Path="Microservice.Common.Tests/Microservice.Common.Tests.csproj" />
|
||||||
|
<Project Path="Microservice.Common/Microservice.Common.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Nacos.V2;
|
using Nacos.V2;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microservice.Common;
|
||||||
|
using Microservice.Common.Models;
|
||||||
|
|
||||||
namespace MicoService.Demo.Controllers
|
namespace MicoService.Demo.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -11,44 +15,74 @@ namespace MicoService.Demo.Controllers
|
||||||
|
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly INacosNamingService _nacosNamingService;
|
private readonly INacosNamingService _nacosNamingService;
|
||||||
|
private readonly IServiceClient _serviceClient;
|
||||||
|
private readonly ILogger<TestController> _logger;
|
||||||
|
|
||||||
public TestController(IConfiguration configuration,
|
public TestController(IConfiguration configuration,
|
||||||
INacosNamingService nacosNamingService)
|
INacosNamingService nacosNamingService,
|
||||||
|
IServiceClient serviceClient,
|
||||||
|
ILogger<TestController> logger)
|
||||||
{
|
{
|
||||||
this._configuration = configuration;
|
this._configuration = configuration;
|
||||||
this._nacosNamingService = nacosNamingService;
|
this._nacosNamingService = nacosNamingService;
|
||||||
|
this._serviceClient = serviceClient;
|
||||||
|
this._logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 演示:读取 Nacos 配置中心配置
|
/// 演示:读取 Nacos 配置中心配置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("config/{key}")]
|
[HttpGet("config/{key}")]
|
||||||
public string GetConfig(string key)
|
public IActionResult GetConfig(string key)
|
||||||
{
|
{
|
||||||
return _configuration[key];
|
var value = _configuration[key];
|
||||||
|
return Ok(ApiResponseHelper.Success(value, "获取配置成功"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 演示:调用其他微服务
|
/// 演示:提供给其他服务调用的接口
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("config/info")]
|
||||||
|
public IActionResult GetConfigInfo()
|
||||||
|
{
|
||||||
|
var data = new ConfigInfoModel("Hello from MicoService.Demo", "This is a test config info");
|
||||||
|
return Ok(ApiResponseHelper.Success(data, "获取配置信息成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 演示:使用微服务通讯客户端调用其他微服务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("call")]
|
[HttpGet("call")]
|
||||||
public async Task CallOtherService()
|
public async Task<IActionResult> CallOtherService()
|
||||||
{
|
{
|
||||||
// 核心API:获取单个实例(内置负载均衡)
|
try
|
||||||
var instance = await _nacosNamingService.SelectOneHealthyInstance("Mico_Demo2222");
|
|
||||||
|
|
||||||
if (instance == null)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("无可用实例");
|
// 使用微服务通讯客户端调用 Mico_Demo2222 服务
|
||||||
|
var result = await this._serviceClient.GetAsync<ConfigInfoModel>("Mico_Demo2222", "/User/config/info");
|
||||||
|
var data = new ServiceCallResultModel<ConfigInfoModel>("调用成功", result);
|
||||||
|
return Ok(ApiResponseHelper.Success(data, "服务调用成功"));
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return StatusCode(ex.StatusCode, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, ex.Message));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, "调用失败: " + ex.Message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拼接调用地址,如:http://192.168.2.10:8080/api/xxx
|
/// <summary>
|
||||||
var callUrl = $"http://{instance.Ip}:{instance.Port}/user/config/info";
|
/// 演示:测试控制器是否正常工作
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("test")]
|
||||||
|
public IActionResult Test()
|
||||||
|
{
|
||||||
|
return Ok(ApiResponseHelper.Success("测试成功", "控制器正常工作"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||||
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
|
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
|
||||||
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
|
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
|
||||||
|
<PackageReference Include="Refit" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
using Nacos.AspNetCore.V2;
|
using Nacos.AspNetCore.V2;
|
||||||
using Nacos.V2.DependencyInjection;
|
using Nacos.V2.DependencyInjection;
|
||||||
|
using Microservice.Common;
|
||||||
|
|
||||||
namespace MicoService.Demo
|
namespace MicoService.Demo
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +29,9 @@ namespace MicoService.Demo
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
// 注册微服务通讯客户端
|
||||||
|
builder.Services.AddServiceClient();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Debug"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microservice.Common;
|
||||||
|
using Microservice.Common.Models;
|
||||||
|
|
||||||
namespace MicoService.Demo2.Controllers
|
namespace MicoService.Demo2.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -7,20 +9,56 @@ namespace MicoService.Demo2.Controllers
|
||||||
public class UserController : ControllerBase
|
public class UserController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
||||||
public UserController( IConfiguration configuration)
|
public UserController( IConfiguration configuration, IServiceClient serviceClient)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_serviceClient = serviceClient;
|
||||||
}
|
}
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IServiceClient _serviceClient;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 演示:读取 Nacos 配置中心配置
|
/// 演示:读取 Nacos 配置中心配置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("config/{key}")]
|
[HttpGet("config/{key}")]
|
||||||
public string GetConfig(string key)
|
public IActionResult GetConfig(string key)
|
||||||
{
|
{
|
||||||
return _configuration[key];
|
var value = _configuration[key];
|
||||||
|
return Ok(ApiResponseHelper.Success(value, "获取配置成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 演示:提供给其他服务调用的接口
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("config/info")]
|
||||||
|
public IActionResult GetConfigInfo()
|
||||||
|
{
|
||||||
|
var data = new ConfigInfoModel("Hello from MicoService.Demo2", "This is a test config info");
|
||||||
|
return Ok(ApiResponseHelper.Success(data, "获取配置信息成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 演示:调用其他微服务
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("call-other")]
|
||||||
|
public async Task<IActionResult> CallOtherService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 使用微服务通讯客户端调用 Mico_Demo1111 服务
|
||||||
|
var result = await _serviceClient.GetAsync<ConfigInfoModel>("Mico_Demo1111", "/test/config/info");
|
||||||
|
var data = new ServiceCallResultModel<ConfigInfoModel>("调用成功", result);
|
||||||
|
return Ok(ApiResponseHelper.Success(data, "服务调用成功"));
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return StatusCode(ex.StatusCode, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, ex.Message));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, "调用失败: " + ex.Message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||||
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
|
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
|
||||||
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
|
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
|
||||||
|
<PackageReference Include="Refit" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
using Nacos.AspNetCore.V2;
|
using Nacos.AspNetCore.V2;
|
||||||
using Nacos.V2.DependencyInjection;
|
using Nacos.V2.DependencyInjection;
|
||||||
|
using Microservice.Common;
|
||||||
|
|
||||||
namespace MicoService.Demo2
|
namespace MicoService.Demo2
|
||||||
{
|
{
|
||||||
|
|
@ -26,6 +27,9 @@ namespace MicoService.Demo2
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
// 注册微服务通讯客户端
|
||||||
|
builder.Services.AddServiceClient();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Nacos.V2;
|
||||||
|
using Nacos.V2.Common;
|
||||||
|
using Nacos.V2.Naming.Dtos;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microservice.Common.Tests
|
||||||
|
{
|
||||||
|
public class ServiceClientTests
|
||||||
|
{
|
||||||
|
private readonly Mock<INacosNamingService> _nacosNamingServiceMock;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly Mock<ILogger<ServiceClient>> _loggerMock;
|
||||||
|
private readonly ServiceClient _serviceClient;
|
||||||
|
|
||||||
|
public ServiceClientTests()
|
||||||
|
{
|
||||||
|
// 初始化模拟对象
|
||||||
|
_nacosNamingServiceMock = new Mock<INacosNamingService>();
|
||||||
|
_loggerMock = new Mock<ILogger<ServiceClient>>();
|
||||||
|
|
||||||
|
// 创建一个测试用的 HttpClient
|
||||||
|
var httpClientHandler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
|
||||||
|
};
|
||||||
|
_httpClient = new HttpClient(httpClientHandler);
|
||||||
|
|
||||||
|
// 创建 ServiceClient 实例
|
||||||
|
_serviceClient = new ServiceClient(_nacosNamingServiceMock.Object, _loggerMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "TestService";
|
||||||
|
var endpoint = "/api/test";
|
||||||
|
var expectedResponse = new { Message = "Test Response" };
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回一个健康的实例
|
||||||
|
var instance = new Instance
|
||||||
|
{
|
||||||
|
Ip = "127.0.0.1",
|
||||||
|
Port = 8080,
|
||||||
|
Healthy = true,
|
||||||
|
Enabled = true
|
||||||
|
};
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
|
||||||
|
|
||||||
|
// 创建一个测试服务器
|
||||||
|
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// 配置测试服务
|
||||||
|
});
|
||||||
|
builder.Configure(app =>
|
||||||
|
{
|
||||||
|
app.MapGet("/api/test", () => Results.Ok(expectedResponse));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用测试服务器的 HttpClient
|
||||||
|
var testHttpClient = testServer.CreateClient();
|
||||||
|
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await testServiceClient.GetAsync<object>(serviceName, endpoint);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_ShouldThrowException_WhenServiceIsNotAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "NonExistentService";
|
||||||
|
var endpoint = "/api/test";
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回 null
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync((Instance)null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<Exception>(() => _serviceClient.GetAsync<object>(serviceName, endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_ShouldHandleDifferentEndpointFormats()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "TestService";
|
||||||
|
var expectedResponse = new { Message = "Test Response" };
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回一个健康的实例
|
||||||
|
var instance = new Instance
|
||||||
|
{
|
||||||
|
Ip = "127.0.0.1",
|
||||||
|
Port = 8080,
|
||||||
|
Healthy = true
|
||||||
|
};
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
|
||||||
|
|
||||||
|
// 创建测试服务器
|
||||||
|
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
// 验证请求路径是否以 '/' 开头
|
||||||
|
Assert.StartsWith("/", context.Request.Path);
|
||||||
|
await context.Response.WriteAsJsonAsync(new { StatusCode = 200, Message = "Success", Data = expectedResponse });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 测试不同格式的端点
|
||||||
|
var endpoints = new[] { "/api/test", "api/test", "test", "/test" };
|
||||||
|
foreach (var endpoint in endpoints)
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
var result = await _serviceClient.GetAsync<object>(serviceName, endpoint);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "TestService";
|
||||||
|
var endpoint = "/api/test";
|
||||||
|
var requestData = new { Name = "Test" };
|
||||||
|
var expectedResponse = new { Message = "Test Response" };
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回一个健康的实例
|
||||||
|
var instance = new Instance
|
||||||
|
{
|
||||||
|
Ip = "127.0.0.1",
|
||||||
|
Port = 8080,
|
||||||
|
Healthy = true,
|
||||||
|
Enabled = true
|
||||||
|
};
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
|
||||||
|
|
||||||
|
// 创建一个测试服务器
|
||||||
|
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.Configure(app =>
|
||||||
|
{
|
||||||
|
app.MapPost("/api/test", (object data) => Results.Ok(expectedResponse));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用测试服务器的 HttpClient
|
||||||
|
var testHttpClient = testServer.CreateClient();
|
||||||
|
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await testServiceClient.PostAsync<object>(serviceName, endpoint, requestData);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PutAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "TestService";
|
||||||
|
var endpoint = "/api/test";
|
||||||
|
var requestData = new { Name = "Test" };
|
||||||
|
var expectedResponse = new { Message = "Test Response" };
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回一个健康的实例
|
||||||
|
var instance = new Instance
|
||||||
|
{
|
||||||
|
Ip = "127.0.0.1",
|
||||||
|
Port = 8080,
|
||||||
|
Healthy = true,
|
||||||
|
Enabled = true
|
||||||
|
};
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
|
||||||
|
|
||||||
|
// 创建一个测试服务器
|
||||||
|
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.Configure(app =>
|
||||||
|
{
|
||||||
|
app.MapPut("/api/test", (object data) => Results.Ok(expectedResponse));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用测试服务器的 HttpClient
|
||||||
|
var testHttpClient = testServer.CreateClient();
|
||||||
|
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await testServiceClient.PutAsync<object>(serviceName, endpoint, requestData);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var serviceName = "TestService";
|
||||||
|
var endpoint = "/api/test";
|
||||||
|
var expectedResponse = new { Message = "Test Response" };
|
||||||
|
|
||||||
|
// 模拟 Nacos 服务发现返回一个健康的实例
|
||||||
|
var instance = new Instance
|
||||||
|
{
|
||||||
|
Ip = "127.0.0.1",
|
||||||
|
Port = 8080,
|
||||||
|
Healthy = true,
|
||||||
|
Enabled = true
|
||||||
|
};
|
||||||
|
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
|
||||||
|
|
||||||
|
// 创建一个测试服务器
|
||||||
|
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.Configure(app =>
|
||||||
|
{
|
||||||
|
app.MapDelete("/api/test", () => Results.Ok(expectedResponse));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用测试服务器的 HttpClient
|
||||||
|
var testHttpClient = testServer.CreateClient();
|
||||||
|
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await testServiceClient.DeleteAsync<object>(serviceName, endpoint);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Microservice.Common.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 异常类
|
||||||
|
/// </summary>
|
||||||
|
public class ApiException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 状态码
|
||||||
|
/// </summary>
|
||||||
|
public int StatusCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statusCode">状态码</param>
|
||||||
|
/// <param name="message">异常消息</param>
|
||||||
|
public ApiException(int statusCode, string message) : base(message)
|
||||||
|
{
|
||||||
|
StatusCode = statusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 标准化 API 响应格式
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
public class ApiResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 状态码
|
||||||
|
/// </summary>
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 响应消息
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时间戳
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据负载
|
||||||
|
/// </summary>
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public ApiResponse()
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statusCode">状态码</param>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
public ApiResponse(int statusCode, string message, T data)
|
||||||
|
{
|
||||||
|
StatusCode = statusCode;
|
||||||
|
Message = message;
|
||||||
|
Data = data;
|
||||||
|
Timestamp = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非泛型 API 响应格式
|
||||||
|
/// </summary>
|
||||||
|
public class ApiResponse : ApiResponse<object>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public ApiResponse() : base()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statusCode">状态码</param>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
public ApiResponse(int statusCode, string message, object data) : base(statusCode, message, data)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 响应工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class ApiResponseHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 生成成功响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> Success<T>(T data, string message = "Success")
|
||||||
|
{
|
||||||
|
return new ApiResponse<T>(
|
||||||
|
statusCode: (int)HttpStatusCode.OK,
|
||||||
|
message: message,
|
||||||
|
data: data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成成功响应(无数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse Success(string message = "Success")
|
||||||
|
{
|
||||||
|
return new ApiResponse(
|
||||||
|
statusCode: (int)HttpStatusCode.OK,
|
||||||
|
message: message,
|
||||||
|
data: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成失败响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="statusCode">状态码</param>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> Error<T>(HttpStatusCode statusCode, string message, T data = default)
|
||||||
|
{
|
||||||
|
return new ApiResponse<T>(
|
||||||
|
statusCode: (int)statusCode,
|
||||||
|
message: message,
|
||||||
|
data: data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成失败响应(无数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statusCode">状态码</param>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse Error(HttpStatusCode statusCode, string message)
|
||||||
|
{
|
||||||
|
return new ApiResponse(
|
||||||
|
statusCode: (int)statusCode,
|
||||||
|
message: message,
|
||||||
|
data: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 400 Bad Request 响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> BadRequest<T>(string message = "Bad Request", T data = default)
|
||||||
|
{
|
||||||
|
return Error(HttpStatusCode.BadRequest, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 401 Unauthorized 响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> Unauthorized<T>(string message = "Unauthorized", T data = default)
|
||||||
|
{
|
||||||
|
return Error(HttpStatusCode.Unauthorized, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 403 Forbidden 响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> Forbidden<T>(string message = "Forbidden", T data = default)
|
||||||
|
{
|
||||||
|
return Error(HttpStatusCode.Forbidden, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 404 Not Found 响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> NotFound<T>(string message = "Not Found", T data = default)
|
||||||
|
{
|
||||||
|
return Error(HttpStatusCode.NotFound, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 500 Internal Server Error 响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="message">响应消息</param>
|
||||||
|
/// <param name="data">数据负载</param>
|
||||||
|
/// <returns>标准化 API 响应</returns>
|
||||||
|
public static ApiResponse<T> InternalServerError<T>(string message = "Internal Server Error", T data = default)
|
||||||
|
{
|
||||||
|
return Error(HttpStatusCode.InternalServerError, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Refit;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用服务 API 接口定义
|
||||||
|
/// </summary>
|
||||||
|
public interface IServiceApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 发送 GET 请求
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="endpoint">API 端点</param>
|
||||||
|
/// <returns>API 响应</returns>
|
||||||
|
[Get("/{**endpoint}")]
|
||||||
|
Task<ApiResponse<T>> GetAsync<T>([AliasAs("endpoint")] string endpoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送 POST 请求
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="endpoint">API 端点</param>
|
||||||
|
/// <param name="data">请求数据</param>
|
||||||
|
/// <returns>API 响应</returns>
|
||||||
|
[Post("/{**endpoint}")]
|
||||||
|
Task<ApiResponse<T>> PostAsync<T>([AliasAs("endpoint")] string endpoint, [Body] object data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送 PUT 请求
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="endpoint">API 端点</param>
|
||||||
|
/// <param name="data">请求数据</param>
|
||||||
|
/// <returns>API 响应</returns>
|
||||||
|
[Put("/{**endpoint}")]
|
||||||
|
Task<ApiResponse<T>> PutAsync<T>([AliasAs("endpoint")] string endpoint, [Body] object data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送 DELETE 请求
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应数据类型</typeparam>
|
||||||
|
/// <param name="endpoint">API 端点</param>
|
||||||
|
/// <returns>API 响应</returns>
|
||||||
|
[Delete("/{**endpoint}")]
|
||||||
|
Task<ApiResponse<T>> DeleteAsync<T>([AliasAs("endpoint")] string endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
public interface IServiceClient
|
||||||
|
{
|
||||||
|
Task<TResponse> GetAsync<TResponse>(string serviceName, string endpoint, object queryParams = null);
|
||||||
|
Task<TResponse> PostAsync<TResponse>(string serviceName, string endpoint, object data);
|
||||||
|
Task<TResponse> PutAsync<TResponse>(string serviceName, string endpoint, object data);
|
||||||
|
Task<TResponse> DeleteAsync<TResponse>(string serviceName, string endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
|
||||||
|
<PackageReference Include="nacos-sdk-csharp" Version="1.3.10" />
|
||||||
|
<PackageReference Include="Polly" Version="7.2.3" />
|
||||||
|
<PackageReference Include="Refit" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Dto\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
namespace Microservice.Common.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置信息模型
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigInfoModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 消息
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置信息
|
||||||
|
/// </summary>
|
||||||
|
public string ConfigInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public ConfigInfoModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="configInfo">配置信息</param>
|
||||||
|
public ConfigInfoModel(string message, string configInfo)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
ConfigInfo = configInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
namespace Microservice.Common.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 服务调用结果模型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResult">结果类型</typeparam>
|
||||||
|
public class ServiceCallResultModel<TResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 消息
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结果
|
||||||
|
/// </summary>
|
||||||
|
public TResult Result { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public ServiceCallResultModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">消息</param>
|
||||||
|
/// <param name="result">结果</param>
|
||||||
|
public ServiceCallResultModel(string message, TResult result)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Nacos.V2;
|
||||||
|
using Nacos.V2.Common;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
using Refit;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
public class ServiceClient : IServiceClient
|
||||||
|
{
|
||||||
|
private readonly INacosNamingService _nacosNamingService;
|
||||||
|
private readonly ILogger<ServiceClient> _logger;
|
||||||
|
private readonly AsyncRetryPolicy _retryPolicy;
|
||||||
|
|
||||||
|
public ServiceClient(INacosNamingService nacosNamingService, ILogger<ServiceClient> logger)
|
||||||
|
{
|
||||||
|
_nacosNamingService = nacosNamingService;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// 配置重试策略
|
||||||
|
_retryPolicy = Policy
|
||||||
|
.Handle<Exception>()
|
||||||
|
.WaitAndRetryAsync(
|
||||||
|
retryCount: 3,
|
||||||
|
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> GetAsync<TResponse>(string serviceName, string endpoint, object queryParams = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await SendRequestAsync<TResponse>(serviceName, endpoint, "GET", null, queryParams);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> PostAsync<TResponse>(string serviceName, string endpoint, object data)
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<TResponse>(serviceName, endpoint, "POST", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> PutAsync<TResponse>(string serviceName, string endpoint, object data)
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<TResponse>(serviceName, endpoint, "PUT", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> DeleteAsync<TResponse>(string serviceName, string endpoint)
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<TResponse>(serviceName, endpoint, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TResponse> SendRequestAsync<TResponse>(string serviceName, string endpoint, string method, object data = null, object queryParams = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从Nacos获取服务实例
|
||||||
|
var instance = await _nacosNamingService.SelectOneHealthyInstance(serviceName);
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"未找到服务 {serviceName} 的可用实例");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建服务基础URL
|
||||||
|
var baseUrl = $"http://{instance.Ip}:{instance.Port}";
|
||||||
|
|
||||||
|
// 创建Refit服务实例
|
||||||
|
var serviceApi = RestService.For<IServiceApi>(baseUrl);
|
||||||
|
|
||||||
|
// 确保endpoint不包含前导的斜杠,因为IServiceApi中的路径模板已经包含了一个斜杠
|
||||||
|
if (!string.IsNullOrEmpty(endpoint))
|
||||||
|
{
|
||||||
|
// 移除所有前导的'/'
|
||||||
|
endpoint = endpoint.TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用重试策略
|
||||||
|
TResponse result = default;
|
||||||
|
await _retryPolicy.ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
ApiResponse<TResponse> apiResponse;
|
||||||
|
|
||||||
|
// 根据HTTP方法调用不同的API
|
||||||
|
switch (method.ToUpper())
|
||||||
|
{
|
||||||
|
case "GET":
|
||||||
|
apiResponse = await serviceApi.GetAsync<TResponse>(endpoint);
|
||||||
|
break;
|
||||||
|
case "POST":
|
||||||
|
apiResponse = await serviceApi.PostAsync<TResponse>(endpoint, data);
|
||||||
|
break;
|
||||||
|
case "PUT":
|
||||||
|
apiResponse = await serviceApi.PutAsync<TResponse>(endpoint, data);
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
apiResponse = await serviceApi.DeleteAsync<TResponse>(endpoint);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"不支持的HTTP方法: {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应状态码
|
||||||
|
if (apiResponse.StatusCode >= 200 && apiResponse.StatusCode < 300)
|
||||||
|
{
|
||||||
|
// 成功响应,返回数据
|
||||||
|
result = apiResponse.Data;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 失败响应,抛出异常
|
||||||
|
throw new ApiException(apiResponse.StatusCode, apiResponse.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (ApiException)
|
||||||
|
{
|
||||||
|
// 重新抛出 ApiException
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microservice.Common
|
||||||
|
{
|
||||||
|
public static class ServiceClientExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddServiceClient(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<IServiceClient, ServiceClient>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue