引入微服务通用通讯客户端及标准化响应
新增 Microservice.Common 通用库,基于 Nacos 和 Refit 实现 IServiceClient,统一微服务间调用方式和 API 响应结构。Demo 与 Demo2 服务集成 ServiceClient,接口返回结构标准化。新增单元测试项目,完善服务间通讯的可测试性。调整依赖与日志配置,提升项目可维护性和扩展性。
This commit is contained in:
parent
590d5ab936
commit
cf3ad910c2
|
|
@ -1,4 +1,6 @@
|
|||
<Solution>
|
||||
<Project Path="MicoService.Demo/MicoService.Demo.csproj" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nacos.V2;
|
||||
using System.Threading.Tasks;
|
||||
using Microservice.Common;
|
||||
using Microservice.Common.Models;
|
||||
|
||||
namespace MicoService.Demo.Controllers
|
||||
{
|
||||
|
|
@ -11,44 +15,74 @@ namespace MicoService.Demo.Controllers
|
|||
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly INacosNamingService _nacosNamingService;
|
||||
private readonly IServiceClient _serviceClient;
|
||||
private readonly ILogger<TestController> _logger;
|
||||
|
||||
public TestController(IConfiguration configuration,
|
||||
INacosNamingService nacosNamingService)
|
||||
INacosNamingService nacosNamingService,
|
||||
IServiceClient serviceClient,
|
||||
ILogger<TestController> logger)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._nacosNamingService = nacosNamingService;
|
||||
this._serviceClient = serviceClient;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 演示:读取 Nacos 配置中心配置
|
||||
/// 演示:读取 Nacos 配置中心配置
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[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.Demo", "This is a test config info");
|
||||
return Ok(ApiResponseHelper.Success(data, "获取配置信息成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示:使用微服务通讯客户端调用其他微服务
|
||||
/// </summary>
|
||||
[HttpGet("call")]
|
||||
public async Task CallOtherService()
|
||||
public async Task<IActionResult> CallOtherService()
|
||||
{
|
||||
// 核心API:获取单个实例(内置负载均衡)
|
||||
var instance = await _nacosNamingService.SelectOneHealthyInstance("Mico_Demo2222");
|
||||
|
||||
if (instance == null)
|
||||
try
|
||||
{
|
||||
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
|
||||
var callUrl = $"http://{instance.Ip}:{instance.Port}/user/config/info";
|
||||
|
||||
/// <summary>
|
||||
/// 演示:测试控制器是否正常工作
|
||||
/// </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="nacos-sdk-csharp.AspNetCore" 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>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
using Nacos.AspNetCore.V2;
|
||||
using Nacos.V2.DependencyInjection;
|
||||
using Microservice.Common;
|
||||
|
||||
namespace MicoService.Demo
|
||||
{
|
||||
|
|
@ -27,6 +28,9 @@ namespace MicoService.Demo
|
|||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// 注册微服务通讯客户端
|
||||
builder.Services.AddServiceClient();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Debug"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microservice.Common;
|
||||
using Microservice.Common.Models;
|
||||
|
||||
namespace MicoService.Demo2.Controllers
|
||||
{
|
||||
|
|
@ -7,20 +9,56 @@ namespace MicoService.Demo2.Controllers
|
|||
public class UserController : ControllerBase
|
||||
{
|
||||
|
||||
public UserController( IConfiguration configuration)
|
||||
public UserController( IConfiguration configuration, IServiceClient serviceClient)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_serviceClient = serviceClient;
|
||||
}
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IServiceClient _serviceClient;
|
||||
/// <summary>
|
||||
/// 演示:读取 Nacos 配置中心配置
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[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="nacos-sdk-csharp.AspNetCore" 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>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
using Nacos.AspNetCore.V2;
|
||||
using Nacos.V2.DependencyInjection;
|
||||
using Microservice.Common;
|
||||
|
||||
namespace MicoService.Demo2
|
||||
{
|
||||
|
|
@ -25,6 +26,9 @@ namespace MicoService.Demo2
|
|||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// 注册微服务通讯客户端
|
||||
builder.Services.AddServiceClient();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
|
|||
|
|
@ -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