29.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--用户配置服务
用户配置服务是孢子记账中最简单的部分。简单说,用户配置服务就是用户自定义的配置项存储服务,用于我们的APP根据用户的配置实现指定的功能。它提供了一个简单的接口,允许用户存储和检索他们的配置数据。就目前来说,用户配置只有一个配置项:默认币种设置。在后续的版本中,我们会根据用户的反馈和需求,添加更多的配置项。
Tip:应大多数读者要求,在这篇文章开始,我们将只讲解每个服务的核心内容。完整代码请访问课程GitHub仓库代码。
一、用户配置服务实现
用户配置服务我们只需要对外开放两个Web Api :获取所有配置、更新配置,我们一起来实现这两个接口。
1.1 获取所有配置
获取所有配置,用于获取当前用户的所有配置项。我们可以通过一个简单的GET请求来实现这个功能。首先,我们需要在IConfigServer
接口中定义一个方法来获取所有配置项。然后,我们需要在ConfigController
中实现这个方法.
在IConfigServer
接口中添加方法定义:
/// <summary>
/// 查询用户配置
/// </summary>
/// <returns>用户配置</returns>
List<ConfigResponse> GetConfig();
接口方法很简单,没必要多讲,我们重点看一下ConfigController
的实现。实现代码如下:
/// <summary>
/// 查询用户配置
/// </summary>
/// <returns>用户配置</returns>
public List<ConfigResponse> GetConfig()
{long userId = _contextSession.UserId;// 查询用户配置List<Config> configs = _context.Configs.Where(c => c.UserId == userId).ToList();// 将配置实体转换为响应模型List<ConfigResponse> configResponses = _autoMapper.Map<List<ConfigResponse>>(configs);return configResponses;
}
在这个方法中,我们首先获取当前用户的ID,然后查询数据库中与该用户相关的所有配置项。最后,我们将查询结果转换为响应模型并返回。
在实际应用中,我们可能需要对配置项进行分页查询,以提高性能和用户体验。这里我们暂时不做分页处理,是因为当前用户配置项不多,待配置型变多后我们会在后续版本会添加。
Tip:代码中的
_contextSession
是当前用户的会话上下文,里面存储了当前用户的ID和用户名,它方便我们在服务中获取当前用户的信息。具体实现我们将在后续的用户会话服务中讲解。在这里我们只需要知道它可以帮助我们获取当前用户的ID即可。
在Controller中我们实现调用GetConfig
方法的逻辑:
/// <summary>
/// 获取所有配置
/// </summary>
/// <returns>用户配置列表</returns>
[HttpGet]
public ActionResult<List<ConfigResponse>> GetConfigs()
{List<ConfigResponse> configs = _configServer.GetConfig();return Ok(configs);
}
在这个方法中,我们调用了_configServer.GetConfig()
来获取所有配置项,并将结果返回给客户端。
1.2 更新配置
更新配置,用于更新当前用户的配置项。我们可以通过一个简单的POST请求来实现这个功能。首先,在IConfigServer
接口中定义一个方法来更新配置项。
/// <summary>
/// 更新用户配置
/// </summary>
/// <param name="config">配置更新请求</param>
void UpdateConfig(ConfigResponse config);
接着,在ConfigController
中实现这个方法。实现代码如下:
/// <summary>
/// 更新用户配置
/// </summary>
/// <param name="config">配置更新请求</param>
public void UpdateConfig(ConfigResponse config)
{long userId = _contextSession.UserId;// 查询用户配置Config? existingConfig = _context.Configs.FirstOrDefault(c => c.Id == config.Id);if (existingConfig == null){throw new NotFoundException("配置项不存在");}existingConfig.Value = config.Value;SettingCommProperty.Edit(existingConfig);_context.Configs.Update(existingConfig);// 保存更改到数据库_context.SaveChanges();
}
在这个方法中,我们首先获取当前用户的ID,然后查询数据库中与该配置项相关的配置项。如果配置项不存在,则抛出一个NotFoundException
异常。接着,我们更新配置项的值,并保存更改到数据库。
在Controller中我们实现调用UpdateConfig
方法的逻辑:
/// <summary>
/// 更新配置
/// </summary>
/// <param name="config">配置更新请求</param>
/// <returns>更新结果</returns>
[HttpPut]
public ActionResult<bool> UpdateConfig([FromBody] ConfigResponse config)
{_configServer.UpdateConfig(config);return Ok(true);
}
在这个方法中,我们调用了_configServer.UpdateConfig(config)
来更新配置项,并将结果返回给客户端。
二、接收用户注册后的配置设置
在这一小节,我们将实现一个功能:当用户注册成功后,自动为用户创建默认的配置项。这样,用户在第一次使用应用时就可以直接使用默认配置,而不需要手动设置。
要实现这个功能有两种方式:一种是用户注册服务中调用配置服务,另一种是直接在通过事件机制。我们先来对比一下这两种方式。
- 用户注册服务调用配置服务:在用户注册服务中,我们可以在用户注册成功后,直接调用配置服务来创建默认配置项。这样做的好处是简单直接,不需要额外的事件机制支持。但是,这种方式会导致用户注册服务和配置服务之间的耦合度较高,增加了系统的复杂性。
- 通过事件机制:我们可以在用户注册成功后,发布一个事件,然后在配置服务中订阅这个事件。当事件被触发时,配置服务会自动创建默认配置项。这种方式的好处是解耦了用户注册服务和配置服务,使得系统更加灵活和可扩展,如果配置服务需要修改或替换,只需要修改配置服务的实现,而不需要修改用户注册服务的代码。
我们在这里选择第二种方式,通过事件机制来实现用户注册后的配置设置,这样可以使得系统更加灵活和可扩展,实现事件机制的方法我们选择使用RabbitMQ消息队列来实现。
首先,我们需要在身份认证服务SP.IdentityService
中的AuthorizationServiceImpl
实现类中的AddUserAsync
方法中发布一个用户注册成功的事件。我们可以在用户注册成功后,使用RabbitMQ的生产者发送一个消息到指定的队列。补充代码如下:
// more codepublic async Task<long> AddUserAsync(UserAddRequest user)
{// more code// 发送mq,设配默认币种MqPublisher publisher = new MqPublisher(newUser.Id.ToString(),MqExchange.UserConfigExchange,MqRoutingKey.UserConfigDefaultCurrencyRoutingKey,MqQueue.UserConfigQueue,MessageType.UserConfigDefaultCurrency,ExchangeType.Direct);await _rabbitMqMessage.SendAsync(publisher);// more code
}
在这段代码中,我们创建了一个MqPublisher
对象,并设置了相关的交换机、路由键和队列。然后,我们使用_rabbitMqMessage.SendAsync(publisher)
方法将消息发送到RabbitMQ。
Tip:MqPublisher的实现代码在
SP.Common
项目中,它是一个简单的消息发布者,用于发送消息到RabbitMQ。它包含了交换机、路由键、队列和消息类型等信息。
接下来,我们需要在配置服务SP.ConfigService
中订阅这个事件,并在收到事件时创建默认配置项。我们可以在配置服务的启动类中添加一个RabbitMQ的消费者来处理这个事件。实现代码如下:
using SP.Common.ExceptionHandling.Exceptions;
using SP.Common.Message.Model;
using SP.Common.Message.Mq;
using SP.Common.Message.Mq.Model;
using SP.ConfigService.Service;namespace SP.ConfigService.Mq;/// <summary>
/// 用户配置默认币种消息消费者服务
/// </summary>
public class UserConfigDefaultCurrencyConsumerService : BackgroundService
{/// <summary>/// RabbitMQ消息处理/// </summary>private readonly RabbitMqMessage _rabbitMqMessage;/// <summary>/// 用户配置服务/// </summary>private readonly IConfigServer _configServer;/// <summary>/// 日志记录器/// </summary>private readonly ILogger<UserConfigDefaultCurrencyConsumerService> _logger;/// <summary>/// 配置/// </summary>private readonly IConfiguration _configuration;/// <summary>/// 用户配置默认币种消息消费者服务构造函数/// </summary>/// <param name="rabbitMqMessage"></param>/// <param name="logger"></param>/// <param name="configuration"></param>public UserConfigDefaultCurrencyConsumerService(RabbitMqMessage rabbitMqMessage, ILogger<UserConfigDefaultCurrencyConsumerService> logger,IConfiguration configuration){_rabbitMqMessage = rabbitMqMessage;_logger = logger;_configuration = configuration;}/// <summary>/// 消费者服务/// </summary>protected override async Task ExecuteAsync(CancellationToken stoppingToken){MqSubscriber subscriber = new MqSubscriber(MqExchange.UserConfigExchange,MqRoutingKey.UserConfigDefaultCurrencyRoutingKey, MqQueue.UserConfigQueue);await _rabbitMqMessage.ReceiveAsync(subscriber, async message =>{MqMessage mqMessage = message as MqMessage;if (mqMessage == null){_logger.LogError("消息转换失败");throw new ArgumentNullException(nameof(mqMessage));}string userId = mqMessage.Body;if (string.IsNullOrEmpty(userId)){_logger.LogError("用户ID不能为空");throw new BusinessException(nameof(userId));}_logger.LogInformation($"接收到用户配置默认币种消息,用户ID: {userId}");if (!long.TryParse(userId, out long parsedUserId)){_logger.LogError("用户ID格式错误");throw new BusinessException("用户ID格式错误");}// 设置用户默认币种,默认币种id从配置文件中获取string defaultCurrencyId = _configuration["DefaultCurrencyId"];_logger.LogInformation($"nacos中配置的默认币种ID: {defaultCurrencyId}");// 调用币种服务设置用户默认币种await _configServer.SetUserDefaultCurrencyAsync(parsedUserId,defaultCurrencyId);});}
}
在这个消费者服务中,我们首先创建了一个MqSubscriber
对象,并设置了相关的交换机、路由键和队列。然后,我们使用_rabbitMqMessage.ReceiveAsync(subscriber, async message => { ... })
方法来接收消息。当收到消息时,我们将消息体转换为MqMessage
对象,并从中获取用户ID。接着,我们调用配置服务的SetUserDefaultCurrencyAsync
方法来设置用户的默认币种。
在SetUserDefaultCurrencyAsync
方法中,我们可以实现设置用户默认币种的逻辑。
IConfigServer
接口中添加方法定义:
/// <summary>
/// 设置用户默认货币
/// </summary>
/// <param name="userId"></param>
/// <param name="defaultCurrencyId"></param>
/// <returns></returns>
Task SetUserDefaultCurrencyAsync(long userId, string defaultCurrencyId);
在ConfigServerImpl
实现类中实现这个方法:
/// <summary>
/// 设置用户默认货币
/// </summary>
/// <param name="userId"></param>
/// <param name="defaultCurrencyId"></param>
/// <returns></returns>
public Task SetUserDefaultCurrencyAsync(long userId, string defaultCurrencyId)
{Config userConfig = new Config();// 更新默认货币IDuserConfig.Value = defaultCurrencyId;userConfig.UserId = userId;userConfig.ConfigType = ConfigTypeEnum.Currency;userConfig.Id = Snow.GetId();userConfig.CreateDateTime = DateTime.Now;userConfig.CreateUserId = userId;_context.Configs.Add(userConfig);// 保存到数据库_context.SaveChanges();return Task.CompletedTask;
}
在这个方法中,我们创建了一个新的Config
对象,并设置其值为默认币种ID。然后,我们将该配置项添加到数据库中,并保存更改。
三、总结
在本节中,我们实现了用户配置服务的核心功能,包括获取所有配置和更新配置。我们还通过事件机制实现了用户注册后自动设置默认币种的功能。这些功能为孢子记账应用提供了基础的用户配置管理能力,使得用户可以根据自己的需求进行个性化设置。