23. 【.NET 8 实战--孢子记账--从单体到微服务】--记账模块--预算

在每个月发工资后很多人会对未来一个月的花销进行大致的计划,这个行为叫做预算。那么在这篇文章中我们将一起开发预算服务。

一、需求

预算需求就是简单的增删改查,虽然比较简单,但是也有几点需要注意。

编号 需求 说明
1 新增预算 1. 针对每种支出类型设置预算;2. 每个用户每种支出类型只能有一条预算
2 删除预算
3 修改预算 1. 不能修改预算的支出类型
4 查询预算
5 预算周期设置 1. 用户可设置预算的周期,按照年、季度、月设置;2. 设置预算的适用范围

根据上面的分析,我们可以得出预算表Budget的核心字段:支出类型的Id、预算金额、预算周期、预算开始时间、预算结束时间。这里要着重说一下为什么有了预算的周期还要有预算的开始时间和结束时间。这是因为用户在设置预算的时候有可能设置的是未来某个时间段内的预算。

二、功能编写

下面我们以新增预算为例,来看一下如何实现新增预算的功能呢。

2.1 编写数据库映射类

根据前面分析的结果,我们编写出了数据库预算表的映射类Budget

csharp 复制代码
using System.ComponentModel.DataAnnotations;
using SporeAccounting.BaseModels;
using System.ComponentModel.DataAnnotations.Schema;

namespace SporeAccounting.Models;

/// <summary>
/// 预算表
/// </summary>
[Table(name: "Budget")]
public class Budget : BaseModel
{
    /// <summary>
    /// 收支类型
    /// </summary>
    [Required]
    [Column(TypeName = "nvarchar(36)")]
    public string IncomeExpenditureClassificationId { get; set; }

    /// <summary>
    /// 预算金额
    /// </summary>
    [Required]
    [Column(TypeName = "decimal(18,2)")]
    public decimal Amount { get; set; }

    /// <summary>
    /// 预算周期
    /// </summary>
    [Column(TypeName = "int")]
    [Required]
    public PeriodEnum Period { get; set; }

    /// <summary>
    /// 剩余预算
    /// </summary>
    [Required]
    [Column(TypeName = "decimal(18,2)")]
    public decimal Remaining { get; set; }

    /// <summary>
    /// 备注
    /// </summary>
    [MaxLength(200)]
    public string? Remark { get; set; }

    /// <summary>
    /// 开始时间
    /// </summary>
    [Required]
    [Column(TypeName = "datetime")]
    public DateTime StartTime { get; set; }

    /// <summary>
    /// 结束时间
    /// </summary>
    [Required]
    [Column(TypeName = "datetime")]
    public DateTime EndTime { get; set; }

    /// <summary>
    /// 用户Id
    /// </summary>
    [Required]
    [Column(TypeName = "nvarchar(36)")]
    [ForeignKey("FK_Budget_SysUser")]
    public string UserId { get; set; }

    /// <summary>
    /// 导航属性
    /// </summary>
    public SysUser SysUser { get; set; }

    /// <summary>
    /// 导航属性
    /// </summary>
    public IncomeExpenditureClassification Classification { get; set; }
        = new IncomeExpenditureClassification();
}

Budget 类我就不做过多的讲解了,大家在编写玩Budget 类后一定要记得将这个类添加到数据库连接上下文类SporeAccountingDBContext中,然后执行数据库迁移命令。

2.2 编写Server服务

我们在Server 文件夹下的Interface 文件夹中新建预算Server接口IBudgetServer,在这个接口中增加新增预算的方法Add,以及判断当前用户是否存在指定支出类型预算的方法IsExistByClassificationId,代码如下"

csharp 复制代码
using SporeAccounting.Models;

namespace SporeAccounting.Server.Interface;

/// <summary>
/// 预算服务
/// </summary>
public interface IBudgetServer
{
    /// <summary>
    /// 添加预算
    /// </summary>
    /// <param name="budget"></param>
    void Add(Budget budget);

    /// <summary>
    /// 用户是否存在该类型预算
    /// </summary>
    /// <param name="classificationId"></param>
    /// <param name="userId"></param>
    /// <returns></returns>
    bool IsExistByClassificationId(string classificationId, string userId);
}

接着,我们实现IBudgetServer接口,在Server 文件夹下创建实现类BudgetImp,代码如下:

csharp 复制代码
using SporeAccounting.Models;
using SporeAccounting.Server.Interface;

namespace SporeAccounting.Server;

/// <summary>
/// 预算服务
/// </summary>
public class BudgetImp : IBudgetServer
{
    /// <summary>
    /// 数据库上下文
    /// </summary>
    private readonly SporeAccountingDBContext _sporeAccountingDbContext;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="sporeAccountingDbContext"></param>
    public BudgetImp(SporeAccountingDBContext sporeAccountingDbContext)
    {
        _sporeAccountingDbContext = sporeAccountingDbContext;
    }

    /// <summary>
    /// 添加预算
    /// </summary>
    /// <param name="budget"></param>
    public void Add(Budget budget)
    {
        try
        {
            _sporeAccountingDbContext.Budgets.Add(budget);
            _sporeAccountingDbContext.SaveChanges();
        }
        catch (Exception e)
        {
            throw;
        }
    }

    /// <summary>
    /// 用户是否存在该类型预算
    /// </summary>
    /// <param name="classificationId"></param>
    /// <param name="userId"></param>
    /// <returns></returns>
    public bool IsExistByClassificationId(string classificationId, string userId)
    {
        try
        {
            return _sporeAccountingDbContext.Budgets.Any(b =>
                b.IncomeExpenditureClassificationId == classificationId && b.UserId == userId);
        }
        catch (Exception e)
        {
            throw;
        }
    }
}

类中的Add方法用于将一个Budget对象添加到数据库中。该方法调用了数据库上下文的Budgets.Add方法将预算对象添加到数据库的追踪列表中,然后通过SaveChanges方法将更改保存到数据库中。IsExistByClassificationId方法的作用是判断某个用户是否已经存在特定分类的预算记录。它接受两个参数:预算分类的IDclassificationId和用户IDuserId,通过_sporeAccountingDbContext.Budgets.Any方法执行数据库查询,返回布尔值。

Server编写完成后别忘了将Budget服务注入到我们的项目中。

2.3 编写新增预算服务接口

最后,我们来编写新增预算的服务接口。我们需要先定义新增预算的视图模型,这个视图模型不需要预算Id,其他的和Budget类一样。

csharp 复制代码
using System.ComponentModel.DataAnnotations;

namespace SporeAccounting.Models.ViewModels;

/// <summary>
/// 预算添加视图模型
/// </summary>
public class BudgetAddViewModel
{
    /// <summary>
    /// 预算金额
    /// </summary>
    [Required(ErrorMessage = "预算金额不能为空")]
    public decimal Amount { get; set; }

    /// <summary>
    /// 周期
    /// </summary>
    [Required(ErrorMessage = "周期不能为空")]
    public PeriodEnum Period { get; set; }

    /// <summary>
    /// 开始时间
    /// </summary>
    [Required(ErrorMessage = "开始时间不能为空")]
    public DateTime StartTime { get; set; }

    /// <summary>
    /// 结束时间
    /// </summary>
    [Required(ErrorMessage = "结束时间不能为空")]
    public DateTime EndTime { get; set; }

    /// <summary>
    /// 收支分类
    /// </summary>
    [Required(ErrorMessage = "收支分类不能为空")]
    public string ClassificationId { get; set; }

    /// <summary>
    /// 备注
    /// </summary>
    [MaxLength(200)]
    public string? Remark { get; set; }
}

接着,我们新建BudgetController,并在增加Add Action。代码如下:

csharp 复制代码
using System.Net;
using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SporeAccounting.BaseModels;
using SporeAccounting.Models;
using SporeAccounting.Models.ViewModels;
using SporeAccounting.Server.Interface;

namespace SporeAccounting.Controllers
{
    /// <summary>
    /// 预算控制器
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class BudgetController : BaseController
    {
        /// <summary>
        /// 预算服务
        /// </summary>
        private IBudgetServer _budgetServer;

        private IMapper _mapper;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="budgetServer"></param>
        /// <param name="mapper"></param>
        public BudgetController(IBudgetServer budgetServer, IMapper mapper)
        {
            _budgetServer = budgetServer;
            _mapper = mapper;
        }

        /// <summary>
        /// 添加预算
        /// </summary>
        /// <param name="budget"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("Add")]
        public ActionResult<ResponseData<bool>> Add([FromBody] BudgetAddViewModel budget)
        {
            try
            {
                string userId = GetUserId();
                // 用户是否存在该类型预算
                bool isExist = _budgetServer.IsExistByClassificationId(budget.ClassificationId, userId);
                if (isExist)
                {
                    return Ok(new ResponseData<bool>(HttpStatusCode.Found, "用户已存在该类型预算", false));
                }
                Budget budgetDb = _mapper.Map<Budget>(budget);
                budgetDb.UserId = userId;
                budgetDb.CreateDateTime = DateTime.Now;
                _budgetServer.Add(budgetDb);
                return Ok(new ResponseData<bool>(HttpStatusCode.OK, "添加成功", true));
            }
            catch (Exception e)
            {
                return Ok(new ResponseData<bool>(HttpStatusCode.InternalServerError, "添加失败", false));
            }
        }
    }
}

这段代码通过_budgetServer.IsExistByClassificationId调用检查该用户是否已经存在相同分类的预算。如果存在,方法立即返回一个状态码为HttpStatusCode.Found的响应,并提示用户预算已存在,同时返回false以指示操作失败。如果预算不存在,代码通过_mapper.Map<Budget>(budget)将前端传递的BudgetAddViewModel对象映射为Budget实体对象。这种映射通常通过AutoMapper等工具完成,简化了DTO(数据传输对象)与实体之间的转换。随后,给新预算实体赋值当前用户的ID以及创建时间,确保数据完整性。在完成数据准备后,调用_budgetServer.Add方法将预算添加到数据库中。

在使用_mapper.Map<Budget>(budget)进行数据转换时我们需要先在SporeAccountingProfile类中配置好转换关系,这里就不多讲了,不清楚的同学请参考专栏一开始的几篇文章,或者上AutoMapper官网学习。

三、总结

这篇文章我们一起编写的预算服务的新增功能,剩余的功能大家自己动手实现,然后下载我的代码来对比一下哪里不一样。

下一篇文章,我们将结合预算和记账功能来完成一个稍微复杂的业务:预算的回退和扣除。

相关推荐
程序猿零零漆4 分钟前
SpringCloud系列教程:微服务的未来(十)服务调用、注册中心原理、Nacos注册中心
java·spring cloud·微服务
深度Linux33 分钟前
Linux性能优化策略:让你的系统运行如飞
linux·运维·性能优化
Damon小智2 小时前
Linux系统中解决端口占用问题
linux·运维·服务器·进程·端口占用
encoding-console2 小时前
centos挂载的基本步骤
linux·运维·centos·挂载
大哥喝阔落2 小时前
linux相关conda操作
linux·运维·conda
豆是浪个2 小时前
Linux(Centos 7.6)命令详解:pwd
linux·运维·服务器
CodeCraft Studio2 小时前
【实用技能】如何使用 .NET C# 中的 Azure Key Vault 中的 PFX 证书对 PDF 文档进行签名
c#·.net·azure
水水阿水水5 小时前
第一章:C++是C语言的扩充(一)
linux·c语言·数据结构·c++·算法
-Harvey8 小时前
ubuntu为Docker配置代理
linux·ubuntu·docker
thehunters8 小时前
win10 ubuntu 使用Android ndk 问题:clang-14: Exec format error
android·linux·ubuntu