diff --git a/MicoService.Demo.slnx b/MicoService.Demo.slnx index e84a1f1..33f9471 100644 --- a/MicoService.Demo.slnx +++ b/MicoService.Demo.slnx @@ -1,4 +1,6 @@ + + diff --git a/MicoService.Demo/Controllers/TestController.cs b/MicoService.Demo/Controllers/TestController.cs index 66bfc08..a7c15d8 100644 --- a/MicoService.Demo/Controllers/TestController.cs +++ b/MicoService.Demo/Controllers/TestController.cs @@ -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 _logger; public TestController(IConfiguration configuration, - INacosNamingService nacosNamingService) + INacosNamingService nacosNamingService, + IServiceClient serviceClient, + ILogger logger) { this._configuration = configuration; this._nacosNamingService = nacosNamingService; + this._serviceClient = serviceClient; + this._logger = logger; } /// - /// ʾȡ Nacos + /// 演示:读取 Nacos 配置中心配置 /// /// /// [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, "获取配置成功")); } /// - /// ʾ΢ + /// 演示:提供给其他服务调用的接口 + /// + [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, "获取配置信息成功")); + } + + /// + /// 演示:使用微服务通讯客户端调用其他微服务 /// [HttpGet("call")] - public async Task CallOtherService() + public async Task CallOtherService() { - // APIȡʵøؾ⣩ - var instance = await _nacosNamingService.SelectOneHealthyInstance("Mico_Demo2222"); - - if (instance == null) + try { - Console.WriteLine("޿ʵ"); + // 使用微服务通讯客户端调用 Mico_Demo2222 服务 + var result = await this._serviceClient.GetAsync("Mico_Demo2222", "/User/config/info"); + var data = new ServiceCallResultModel("调用成功", 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"; - + /// + /// 演示:测试控制器是否正常工作 + /// + [HttpGet("test")] + public IActionResult Test() + { + return Ok(ApiResponseHelper.Success("测试成功", "控制器正常工作")); } diff --git a/MicoService.Demo/MicoService.Demo.csproj b/MicoService.Demo/MicoService.Demo.csproj index 25d4627..58aee2c 100644 --- a/MicoService.Demo/MicoService.Demo.csproj +++ b/MicoService.Demo/MicoService.Demo.csproj @@ -10,6 +10,11 @@ + + + + + diff --git a/MicoService.Demo/Program.cs b/MicoService.Demo/Program.cs index e639217..47c62e3 100644 --- a/MicoService.Demo/Program.cs +++ b/MicoService.Demo/Program.cs @@ -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(); diff --git a/MicoService.Demo/appsettings.json b/MicoService.Demo/appsettings.json index 10f68b8..81d2855 100644 --- a/MicoService.Demo/appsettings.json +++ b/MicoService.Demo/appsettings.json @@ -1,8 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug", + "Microsoft.AspNetCore": "Debug" } }, "AllowedHosts": "*" diff --git a/MicoService.Demo2/Controllers/UserController.cs b/MicoService.Demo2/Controllers/UserController.cs index 9778b80..18e431f 100644 --- a/MicoService.Demo2/Controllers/UserController.cs +++ b/MicoService.Demo2/Controllers/UserController.cs @@ -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; /// /// 演示:读取 Nacos 配置中心配置 /// /// /// [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, "获取配置成功")); + } + + /// + /// 演示:提供给其他服务调用的接口 + /// + [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, "获取配置信息成功")); + } + + /// + /// 演示:调用其他微服务 + /// + [HttpGet("call-other")] + public async Task CallOtherService() + { + try + { + // 使用微服务通讯客户端调用 Mico_Demo1111 服务 + var result = await _serviceClient.GetAsync("Mico_Demo1111", "/test/config/info"); + var data = new ServiceCallResultModel("调用成功", 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)); + } } diff --git a/MicoService.Demo2/MicoService.Demo2.csproj b/MicoService.Demo2/MicoService.Demo2.csproj index 25d4627..58aee2c 100644 --- a/MicoService.Demo2/MicoService.Demo2.csproj +++ b/MicoService.Demo2/MicoService.Demo2.csproj @@ -10,6 +10,11 @@ + + + + + diff --git a/MicoService.Demo2/Program.cs b/MicoService.Demo2/Program.cs index ffc9230..d4e835c 100644 --- a/MicoService.Demo2/Program.cs +++ b/MicoService.Demo2/Program.cs @@ -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(); diff --git a/Microservice.Common.Tests/Microservice.Common.Tests.csproj b/Microservice.Common.Tests/Microservice.Common.Tests.csproj new file mode 100644 index 0000000..939afb9 --- /dev/null +++ b/Microservice.Common.Tests/Microservice.Common.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microservice.Common.Tests/ServiceClientTests.cs b/Microservice.Common.Tests/ServiceClientTests.cs new file mode 100644 index 0000000..58f5b09 --- /dev/null +++ b/Microservice.Common.Tests/ServiceClientTests.cs @@ -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 _nacosNamingServiceMock; + private readonly HttpClient _httpClient; + private readonly Mock> _loggerMock; + private readonly ServiceClient _serviceClient; + + public ServiceClientTests() + { + // 初始化模拟对象 + _nacosNamingServiceMock = new Mock(); + _loggerMock = new Mock>(); + + // 创建一个测试用的 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().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(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(() => _serviceClient.GetAsync(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().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(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().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(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().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(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().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(serviceName, endpoint); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedResponse.Message, ((dynamic)result).Message); + } + } +} \ No newline at end of file diff --git a/Microservice.Common.Tests/UnitTest1.cs b/Microservice.Common.Tests/UnitTest1.cs new file mode 100644 index 0000000..2df2826 --- /dev/null +++ b/Microservice.Common.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Microservice.Common.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/Microservice.Common/ApiException.cs b/Microservice.Common/ApiException.cs new file mode 100644 index 0000000..2b6290c --- /dev/null +++ b/Microservice.Common/ApiException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Microservice.Common +{ + /// + /// API 异常类 + /// + public class ApiException : Exception + { + /// + /// 状态码 + /// + public int StatusCode { get; } + + /// + /// 构造函数 + /// + /// 状态码 + /// 异常消息 + public ApiException(int statusCode, string message) : base(message) + { + StatusCode = statusCode; + } + } +} \ No newline at end of file diff --git a/Microservice.Common/ApiResponse.cs b/Microservice.Common/ApiResponse.cs new file mode 100644 index 0000000..7d39bdb --- /dev/null +++ b/Microservice.Common/ApiResponse.cs @@ -0,0 +1,74 @@ +using System; + +namespace Microservice.Common +{ + /// + /// 标准化 API 响应格式 + /// + /// 响应数据类型 + public class ApiResponse + { + /// + /// 状态码 + /// + public int StatusCode { get; set; } + + /// + /// 响应消息 + /// + public string Message { get; set; } + + /// + /// 时间戳 + /// + public DateTime Timestamp { get; set; } + + /// + /// 数据负载 + /// + public T Data { get; set; } + + /// + /// 构造函数 + /// + public ApiResponse() + { + Timestamp = DateTime.UtcNow; + } + + /// + /// 构造函数 + /// + /// 状态码 + /// 响应消息 + /// 数据负载 + public ApiResponse(int statusCode, string message, T data) + { + StatusCode = statusCode; + Message = message; + Data = data; + Timestamp = DateTime.UtcNow; + } + } + + /// + /// 非泛型 API 响应格式 + /// + public class ApiResponse : ApiResponse + { + /// + /// 构造函数 + /// + public ApiResponse() : base() + { } + + /// + /// 构造函数 + /// + /// 状态码 + /// 响应消息 + /// 数据负载 + public ApiResponse(int statusCode, string message, object data) : base(statusCode, message, data) + { } + } +} \ No newline at end of file diff --git a/Microservice.Common/ApiResponseHelper.cs b/Microservice.Common/ApiResponseHelper.cs new file mode 100644 index 0000000..e72896e --- /dev/null +++ b/Microservice.Common/ApiResponseHelper.cs @@ -0,0 +1,132 @@ +using System.Net; + +namespace Microservice.Common +{ + /// + /// API 响应工具类 + /// + public static class ApiResponseHelper + { + /// + /// 生成成功响应 + /// + /// 响应数据类型 + /// 数据负载 + /// 响应消息 + /// 标准化 API 响应 + public static ApiResponse Success(T data, string message = "Success") + { + return new ApiResponse( + statusCode: (int)HttpStatusCode.OK, + message: message, + data: data + ); + } + + /// + /// 生成成功响应(无数据) + /// + /// 响应消息 + /// 标准化 API 响应 + public static ApiResponse Success(string message = "Success") + { + return new ApiResponse( + statusCode: (int)HttpStatusCode.OK, + message: message, + data: null + ); + } + + /// + /// 生成失败响应 + /// + /// 响应数据类型 + /// 状态码 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse Error(HttpStatusCode statusCode, string message, T data = default) + { + return new ApiResponse( + statusCode: (int)statusCode, + message: message, + data: data + ); + } + + /// + /// 生成失败响应(无数据) + /// + /// 状态码 + /// 响应消息 + /// 标准化 API 响应 + public static ApiResponse Error(HttpStatusCode statusCode, string message) + { + return new ApiResponse( + statusCode: (int)statusCode, + message: message, + data: null + ); + } + + /// + /// 生成 400 Bad Request 响应 + /// + /// 响应数据类型 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse BadRequest(string message = "Bad Request", T data = default) + { + return Error(HttpStatusCode.BadRequest, message, data); + } + + /// + /// 生成 401 Unauthorized 响应 + /// + /// 响应数据类型 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse Unauthorized(string message = "Unauthorized", T data = default) + { + return Error(HttpStatusCode.Unauthorized, message, data); + } + + /// + /// 生成 403 Forbidden 响应 + /// + /// 响应数据类型 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse Forbidden(string message = "Forbidden", T data = default) + { + return Error(HttpStatusCode.Forbidden, message, data); + } + + /// + /// 生成 404 Not Found 响应 + /// + /// 响应数据类型 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse NotFound(string message = "Not Found", T data = default) + { + return Error(HttpStatusCode.NotFound, message, data); + } + + /// + /// 生成 500 Internal Server Error 响应 + /// + /// 响应数据类型 + /// 响应消息 + /// 数据负载 + /// 标准化 API 响应 + public static ApiResponse InternalServerError(string message = "Internal Server Error", T data = default) + { + return Error(HttpStatusCode.InternalServerError, message, data); + } + } +} \ No newline at end of file diff --git a/Microservice.Common/IServiceApi.cs b/Microservice.Common/IServiceApi.cs new file mode 100644 index 0000000..b889228 --- /dev/null +++ b/Microservice.Common/IServiceApi.cs @@ -0,0 +1,49 @@ +using Refit; +using System.Threading.Tasks; + +namespace Microservice.Common +{ + /// + /// 通用服务 API 接口定义 + /// + public interface IServiceApi + { + /// + /// 发送 GET 请求 + /// + /// 响应数据类型 + /// API 端点 + /// API 响应 + [Get("/{**endpoint}")] + Task> GetAsync([AliasAs("endpoint")] string endpoint); + + /// + /// 发送 POST 请求 + /// + /// 响应数据类型 + /// API 端点 + /// 请求数据 + /// API 响应 + [Post("/{**endpoint}")] + Task> PostAsync([AliasAs("endpoint")] string endpoint, [Body] object data); + + /// + /// 发送 PUT 请求 + /// + /// 响应数据类型 + /// API 端点 + /// 请求数据 + /// API 响应 + [Put("/{**endpoint}")] + Task> PutAsync([AliasAs("endpoint")] string endpoint, [Body] object data); + + /// + /// 发送 DELETE 请求 + /// + /// 响应数据类型 + /// API 端点 + /// API 响应 + [Delete("/{**endpoint}")] + Task> DeleteAsync([AliasAs("endpoint")] string endpoint); + } +} diff --git a/Microservice.Common/IServiceClient.cs b/Microservice.Common/IServiceClient.cs new file mode 100644 index 0000000..29d2f20 --- /dev/null +++ b/Microservice.Common/IServiceClient.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Microservice.Common +{ + public interface IServiceClient + { + Task GetAsync(string serviceName, string endpoint, object queryParams = null); + Task PostAsync(string serviceName, string endpoint, object data); + Task PutAsync(string serviceName, string endpoint, object data); + Task DeleteAsync(string serviceName, string endpoint); + } +} \ No newline at end of file diff --git a/Microservice.Common/Microservice.Common.csproj b/Microservice.Common/Microservice.Common.csproj new file mode 100644 index 0000000..9839b2b --- /dev/null +++ b/Microservice.Common/Microservice.Common.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Microservice.Common/Models/ConfigInfoModel.cs b/Microservice.Common/Models/ConfigInfoModel.cs new file mode 100644 index 0000000..d26f8e4 --- /dev/null +++ b/Microservice.Common/Models/ConfigInfoModel.cs @@ -0,0 +1,36 @@ +namespace Microservice.Common.Models +{ + /// + /// 配置信息模型 + /// + public class ConfigInfoModel + { + /// + /// 消息 + /// + public string Message { get; set; } + + /// + /// 配置信息 + /// + public string ConfigInfo { get; set; } + + /// + /// 构造函数 + /// + public ConfigInfoModel() + { + } + + /// + /// 构造函数 + /// + /// 消息 + /// 配置信息 + public ConfigInfoModel(string message, string configInfo) + { + Message = message; + ConfigInfo = configInfo; + } + } +} diff --git a/Microservice.Common/Models/ServiceCallResultModel.cs b/Microservice.Common/Models/ServiceCallResultModel.cs new file mode 100644 index 0000000..cee6f2c --- /dev/null +++ b/Microservice.Common/Models/ServiceCallResultModel.cs @@ -0,0 +1,37 @@ +namespace Microservice.Common.Models +{ + /// + /// 服务调用结果模型 + /// + /// 结果类型 + public class ServiceCallResultModel + { + /// + /// 消息 + /// + public string Message { get; set; } + + /// + /// 结果 + /// + public TResult Result { get; set; } + + /// + /// 构造函数 + /// + public ServiceCallResultModel() + { + } + + /// + /// 构造函数 + /// + /// 消息 + /// 结果 + public ServiceCallResultModel(string message, TResult result) + { + Message = message; + Result = result; + } + } +} diff --git a/Microservice.Common/ServiceClient.cs b/Microservice.Common/ServiceClient.cs new file mode 100644 index 0000000..af19afd --- /dev/null +++ b/Microservice.Common/ServiceClient.cs @@ -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 _logger; + private readonly AsyncRetryPolicy _retryPolicy; + + public ServiceClient(INacosNamingService nacosNamingService, ILogger logger) + { + _nacosNamingService = nacosNamingService; + _logger = logger; + + // 配置重试策略 + _retryPolicy = Policy + .Handle() + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)) + ); + } + + public async Task GetAsync(string serviceName, string endpoint, object queryParams = null) + { + try + { + var result = await SendRequestAsync(serviceName, endpoint, "GET", null, queryParams); + return result; + } + catch (Exception) + { + throw; + } + } + + public async Task PostAsync(string serviceName, string endpoint, object data) + { + return await SendRequestAsync(serviceName, endpoint, "POST", data); + } + + public async Task PutAsync(string serviceName, string endpoint, object data) + { + return await SendRequestAsync(serviceName, endpoint, "PUT", data); + } + + public async Task DeleteAsync(string serviceName, string endpoint) + { + return await SendRequestAsync(serviceName, endpoint, "DELETE"); + } + + private async Task SendRequestAsync(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(baseUrl); + + // 确保endpoint不包含前导的斜杠,因为IServiceApi中的路径模板已经包含了一个斜杠 + if (!string.IsNullOrEmpty(endpoint)) + { + // 移除所有前导的'/' + endpoint = endpoint.TrimStart('/'); + } + + // 使用重试策略 + TResponse result = default; + await _retryPolicy.ExecuteAsync(async () => + { + ApiResponse apiResponse; + + // 根据HTTP方法调用不同的API + switch (method.ToUpper()) + { + case "GET": + apiResponse = await serviceApi.GetAsync(endpoint); + break; + case "POST": + apiResponse = await serviceApi.PostAsync(endpoint, data); + break; + case "PUT": + apiResponse = await serviceApi.PutAsync(endpoint, data); + break; + case "DELETE": + apiResponse = await serviceApi.DeleteAsync(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; + } + } + } +} \ No newline at end of file diff --git a/Microservice.Common/ServiceClientExtensions.cs b/Microservice.Common/ServiceClientExtensions.cs new file mode 100644 index 0000000..aa9e8a0 --- /dev/null +++ b/Microservice.Common/ServiceClientExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Microservice.Common +{ + public static class ServiceClientExtensions + { + public static IServiceCollection AddServiceClient(this IServiceCollection services) + { + services.AddTransient(); + + return services; + } + } +} \ No newline at end of file