31.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--财务服务--收支分类
从这篇文章开始,我们将正式进入孢子记账应用的核心模块——财务服务的开发阶段。财务服务是整个记账系统的基础,主要包括四大功能模块:收支分类、预算管理、账本管理以及记账功能。每个模块都承担着不同的职责,共同支撑起完整的财务流程。
在本篇文章中,我们将重点介绍“收支分类”这一功能。收支分类是财务服务的起点,通过合理的分类,可以帮助用户清晰地了解自己的收入和支出结构,为后续的预算制定和账本管理打下坚实的基础。
一、为什么要把收支分类功能放在财务服务中?
看到这里,一定有很多人会问,为什么要将收支分类功能放在财务服务中,而不是单独作为一个模块?这是因为收支分类不仅仅是一个简单的分类功能,它与整个记账流程息息相关。通过将其纳入财务服务的优点是,将收支分类与其他财务功能整合,可以实现数据的统一管理,避免数据孤岛。收支分类的数据可以被预算管理和账本管理等其他模块共享,提升系统的整体效率和数据一致性。
具体来说,收支分类作为财务服务的基础模块,能够为后续的预算制定、账本统计和记账操作提供标准化的数据支持。例如,在预算管理中,用户可以针对不同的收支类别设置预算限额,实现更精细化的财务控制;在账本管理中,系统可以根据收支分类自动归集和统计数据,帮助用户快速分析财务状况。这样一来,收支分类不仅提升了系统的可扩展性,也为后续功能的开发和维护带来了便利。
当然,我们也可以将收支分类作为一个独立的模块,但这样会导致数据分散,增加系统的复杂性和维护成本。独立模块虽然在某些场景下有助于灵活扩展,但在记账系统中,收支分类与财务服务的高度耦合决定了将其集成到财务服务中更为合理。最终,是否将收支分类作为独立模块还是集成到财务服务中,取决于系统的设计需求和架构规划,需要根据实际业务场景进行权衡和选择。
二、功能设计
功能设计我们从孢子记账的单体应用代码中提取出收支分类的相关功能,功能包括:获取指定分类下的所有子分类、更新收支分类、批量更新父级分类、批量删除收支分类。这四个功能是收支分类模块的核心操作,对于我们的系统来说基本足够了。
2.1 控制器
Controller相关的代码直接迁移单体应用的代码,然后修改控制器名称以及控制器路由地址即可,代码如下:
using Microsoft.AspNetCore.Mvc;
using SP.FinanceService.Models.Request;
using SP.FinanceService.Models.Response;
using SP.FinanceService.Service;namespace SP.FinanceService.Controllers
{/// <summary>/// 收支分类接口/// </summary>[Route("/api/transaction-categories")][ApiController]public class TransactionCategoryController : ControllerBase{/// <summary>/// 收支分类服务/// </summary>private readonly ITransactionCategoryServer _transactionCategoryServer;/// <summary>/// 收支分类控制器构造函数/// </summary>/// <param name="transactionCategoryServer"></param>public TransactionCategoryController(ITransactionCategoryServer transactionCategoryServer){_transactionCategoryServer = transactionCategoryServer;}/// <summary>/// 获取指定分类下的所有子分类/// </summary>/// <param name="parentId">父分类ID</param>/// <returns>返回子分类列表</returns>[HttpGet("by-parent/{parentId}")]public ActionResult<List<TransactionCategoryResponse>> GetCategoriesByParent([FromRoute] long parentId){List<TransactionCategoryResponse> categories = _transactionCategoryServer.QueryByParentId(parentId);return Ok(categories);}/// <summary>/// 更新收支分类/// </summary>/// <param name="id">分类ID</param>/// <param name="category">收支分类信息</param>/// <returns>返回修改结果</returns>[HttpPut("{id}")]public ActionResult<bool> UpdateCategory([FromRoute] long id, [FromBody] TransactionCategoryEditRequest category){if (category == null || category.Id <= 0){return BadRequest("Invalid category data.");}bool result = _transactionCategoryServer.Edit(category);if (result){return Ok(true);}else{return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update category.");}}/// <summary>/// 批量更新父级分类/// </summary>/// <param name="category">修改父级分类信息</param>/// <returns>返回修改结果</returns>[HttpPut("update-parent")]public ActionResult<bool> UpdateParentCategory([FromBody] TransactionCategoryParentEditRequest category){bool result = _transactionCategoryServer.EditParent(category);if (result){return Ok(true);}else{return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update parent category.");}}/// <summary>/// 批量删除收支分类/// </summary>/// <param name="categoryIds">要删除的分类ID列表</param>/// <returns>返回删除结果</returns>[HttpDelete("batch")]public ActionResult<bool> DeleteCategories([FromBody] List<long> categoryIds){var result = _transactionCategoryServer.Delete(categoryIds);return Ok(result);}}
}
在上述代码中,我们定义了一个 TransactionCategoryController
控制器,包含了四个主要的 API 接口:获取子分类、更新分类、批量更新父级分类和批量删除分类。每个接口都对应一个具体的业务逻辑方法,这些方法将调用服务层的相应功能实现。与单体应用相比,我们只需要修改控制器的路由地址和名称,其他代码基本保持不变。
2.2 服务层
服务层的代码同样直接迁移自单体应用的代码,主要负责处理业务逻辑。我们需要确保服务层能够正确处理来自控制器的请求,并返回相应的结果。服务层接口定义如下:
using SP.FinanceService.Models.Entity;
using SP.FinanceService.Models.Request;
using SP.FinanceService.Models.Response;namespace SP.FinanceService.Service;/// <summary>
/// 收支分类服务接口
/// </summary>
public interface ITransactionCategoryServer
{/// <summary>/// 查询所有收支分类/// </summary>/// <param name="parentId">父分类id</param>/// <returns>返回子分类列表</returns>List<TransactionCategoryResponse> QueryByParentId(long parentId);/// <summary>/// 修改收支分类/// </summary>/// <param name="category">收支分类信息</param>/// <returns>返回修改结果</returns>bool Edit(TransactionCategoryEditRequest category);/// <summary>/// 批量修改父级分类/// </summary>/// <param name="category">修改父级分类信息</param>/// <returns>返回修改结果</returns>bool EditParent(TransactionCategoryParentEditRequest category);/// <summary>/// 批量删除收支分类/// </summary>/// <param name="categoryIds">收支分类ID列表</param>/// <returns></returns>bool Delete(List<long> categoryIds);/// <summary>/// 查询分类信息/// </summary>/// <param name="categoryId">分类id</param>/// <returns>返回分类信息</returns>TransactionCategory? QueryById(long categoryId);
}
服务层接口定义了四个主要方法:查询子分类、修改分类、批量修改父级分类和批量删除分类。每个方法都对应一个具体的业务逻辑操作,这些方法将被控制器调用。
服务层的实现类需要实现这些接口方法,具体的业务逻辑同样是直接从单体应用中迁移过来。以下是服务层的实现代码:
using AutoMapper;
using SP.Common.ExceptionHandling.Exceptions;
using SP.Common.Model;
using SP.FinanceService.DB;
using SP.FinanceService.Models.Entity;
using SP.FinanceService.Models.Request;
using SP.FinanceService.Models.Response;namespace SP.FinanceService.Service.Impl;/// <summary>
/// 收支分类服务实现
/// </summary>
public class TransactionCategoryServerImpl : ITransactionCategoryServer
{private readonly IMapper _automapper;/// <summary>/// 数据库上下文/// </summary>private readonly FinanceServiceDbContext _dbContext;/// <summary>/// 收支分类服务构造函数/// </summary>/// <param name="dbContext"></param>public TransactionCategoryServerImpl(FinanceServiceDbContext dbContext, IMapper automapper){_dbContext = dbContext;_automapper = automapper;}/// <summary>/// 查询所有收支分类/// </summary>/// <param name="parentId">父分类id</param>/// <returns>返回子分类列表</returns>public List<TransactionCategoryResponse> QueryByParentId(long parentId){// 查询指定父分类下的所有子分类var categories = _dbContext.TransactionCategories.Where(c => c.ParentId == parentId).ToList();List<TransactionCategoryResponse> categoryResponses =_automapper.Map<List<TransactionCategoryResponse>>(categories);return categoryResponses;}/// <summary>/// 修改收支分类/// </summary>/// <param name="category">收支分类信息</param>/// <returns>返回修改结果</returns>public bool Edit(TransactionCategoryEditRequest category){// 查询要修改的分类var existingCategory = QueryById(category.Id);if (existingCategory == null){throw new NotFoundException($"分类不存在,ID: {category.Id}");}existingCategory.Name = category.Name;// 保存更改到数据库_dbContext.SaveChanges();return true;}/// <summary>/// 批量修改父级分类/// </summary>/// <param name="category">修改父级分类信息</param>/// <returns>返回修改结果</returns>public bool EditParent(TransactionCategoryParentEditRequest category){List<TransactionCategory> existingCategories = QueryByIds(category.Id);if (existingCategories == null || !existingCategories.Any()){throw new NotFoundException("未找到指定的收支分类");}// 父级分类是否存在var parentCategory = QueryById(category.ParentId);if (parentCategory == null){throw new NotFoundException($"父级分类不存在,ID: {category.ParentId}");}// 判断是否存在不能修改的分类var cannotEdit = existingCategories.Where(c => !c.CanDelete).ToList();if (cannotEdit.Any()){var names = string.Join(",", cannotEdit.Select(c => c.Name));// 抛出业务异常,提示不能修改的分类名称throw new BusinessException($"以下分类不能修改父级分类:{names}");}// 检查是否将分类的父级ID设置为自身ID,防止循环引用if (existingCategories.Any(c => c.Id == category.ParentId)){throw new BusinessException("不能将分类的父级ID设置为自身ID,防止循环引用");}// 检查父级分类与子分类的类型是否一致if (existingCategories.Any(c => c.Type != parentCategory.Type)){throw new BusinessException("修改父类时不能指定不同类型的分类作为父类");}// 开启事务using var transaction = _dbContext.Database.BeginTransaction();// 修改每个分类的父级IDforeach (var existingCategory in existingCategories.Where(c => c.CanDelete)){existingCategory.ParentId = category.ParentId;}// 保存更改到数据库_dbContext.SaveChanges();return true;}/// <summary>/// 批量删除收支分类/// </summary>/// <param name="categoryIds">分类Id集合</param>/// <returns></returns>public bool Delete(List<long> categoryIds){if (categoryIds == null || !categoryIds.Any()){throw new BusinessException("分类ID列表不能为空");}// 查询要删除的分类var categoriesToDelete = QueryByIds(categoryIds);if (categoriesToDelete == null || !categoriesToDelete.Any()){throw new NotFoundException("未找到指定的收支分类");}// 检查是否存在不能删除的分类var cannotDelete = categoriesToDelete.Where(c => !c.CanDelete).ToList();if (cannotDelete.Any()){var names = string.Join(",", cannotDelete.Select(c => c.Name));// 抛出业务异常,提示不能删除的分类名称throw new BusinessException($"以下分类不能删除:{names}");}// 执行删除foreach (var category in categoriesToDelete){SettingCommProperty.Delete(category);}// 保存更改到数据库_dbContext.SaveChanges();return true;}/// <summary>/// 查询分类信息/// </summary>/// <param name="categoryId">分类id</param>/// <returns>返回分类信息</returns>public TransactionCategory? QueryById(long categoryId){return _dbContext.TransactionCategories.FirstOrDefault(c => c.Id == categoryId && c.IsDeleted == false);}/// <summary>/// 根据id列表批量查询收支分类/// </summary>/// <param name="ids">分类ID列表</param>/// <returns>返回收支分类列表</returns>private List<TransactionCategory> QueryByIds(List<long>? ids){if (ids == null || !ids.Any()){return new List<TransactionCategory>();}// 查询指定ID列表的收支分类return _dbContext.TransactionCategories.Where(c => ids.Contains(c.Id) && c.IsDeleted == false).ToList();}
}
在服务层实现中,我们使用了 Entity Framework Core 来操作数据库。每个方法都实现了具体的业务逻辑,例如查询分类、修改分类、批量修改父级分类和批量删除分类。我们还使用了 AutoMapper 来简化实体与 DTO 之间的转换。
这些代码都是直接从单体应用中迁移过来的,主要是为了保持业务逻辑的一致性和可维护性。通过将这些功能集成到财务服务中,我们可以确保收支分类的操作能够与其他财务功能无缝衔接。与其他模块相比,收支分类功能的代码几乎没做什么变动,主要是调整了命名和路由地址,以适应新的微服务架构。
这种迁移方式有几个显著优势。首先,能够最大程度地复用已有的成熟代码,减少开发和测试的工作量。其次,业务逻辑保持一致,有助于后续的维护和升级,避免因重构导致的潜在问题。
当然,迁移并不是终点。在后续的优化阶段,我们会根据微服务架构的特点,对这些代码进行进一步的优化和重构。同时,还可以针对性能瓶颈进行专项优化,如数据库索引、缓存机制等,确保系统在高并发场景下依然稳定高效。
总之,收支分类功能的迁移只是微服务化改造的第一步。随着系统架构的不断演进,我们会持续优化和完善相关代码,提升整体性能和可维护性,为后续的预算管理、账本管理等模块的微服务化提供坚实的基础。
三、总结
在本篇文章中,我们详细介绍了孢子记账应用的财务服务模块中的收支分类功能的设计和实现。通过将收支分类功能集成到财务服务中,我们不仅提升了系统的可扩展性和数据一致性,还为后续的预算管理和账本管理等功能打下了坚实的基础。
在接下来的文章中,我们将继续深入探讨财务服务的其他功能模块,包括预算管理和账本管理等。每个模块都将围绕着如何更好地服务于用户的财务需求展开,确保系统能够提供全面、便捷的记账体验。
通过本篇文章的学习,相信大家对收支分类功能的设计思路和实现细节有了更深入的了解。希望大家能够在实际开发中灵活运用这些知识,不断优化和完善自己的应用程序,为用户提供更好的服务体验。
在下一篇文章中,我们将继续探索财务服务的其他功能模块,敬请期待!同时,也欢迎大家在评论区分享自己的想法和建议,让我们一起交流学习,共同进步。
如果你对孢子记账应用的微服务化改造有任何疑问或建议,欢迎在评论区留言,我们会尽快回复。同时,也欢迎大家关注我们的后续文章,了解更多关于孢子记账应用的开发和优化内容。感谢大家的支持与关注,让我们一起期待下一篇文章的发布!