27. 【.NET 8 实战--孢子记账--从单体到微服务】--简易报表--报表服务
报表是每个记账应用所具备的功能,要实现报表功能就需要把账本的核心功能(记账)完成,因此报表服务作为本专栏第一部分单体应用开发中最后一个要实现的功能,这一篇文章很简单,我们一起来实现一个简单的报表服务。
一、需求
需求很简单,我们只需要提供一个接口供客户端查询时使用,下面是需求。
编号 | 需求 | 说明 |
---|---|---|
1 | 报表查询 | 1. 传入报表类型,年份、月份查询对应的报表数据;2. 月份参数可以为空;3. 报表类型包括:月报表、季度报表、年报表 |
二、功能编写
-
模型编写
根据上一小节的需求,我们得出了传入的参数需要包含:报表类型、年份以及月份,其中月份可以为空,视图模型如下:
using System.ComponentModel.DataAnnotations; namespace SporeAccounting.Models.ViewModels; /// <summary> /// 报表视图模型 /// </summary> public class ReportViewModel { /// <summary> /// 报表类型 /// </summary> [Required(ErrorMessage = "报表类型不能为空")] public ReportTypeEnum ReportType { get; set; } /// <summary> /// 年份 /// </summary> [Required(ErrorMessage = "年份不能为空")] public int Year { get; set; } /// <summary> /// 月份 /// </summary> public int? Month { get; set; } }
我们一起来看看数据库表映射类需要包含哪些属性吧。首先,需要年份、月份和季度这几个字段,它们可以提供清晰的时间维度,方便用户在不同时间范围内查询和统计数据,比如生成年度报告、月度账单或季度分析等。
其次,需要一个主题或用途字段,用于标识报表的具体内容。此外,还需要一个报表类型字段,可以用枚举值来区分不同类型的报表(如季度报表、月度报表、年度报表)。
因为报表通常涉及金额,所以必须有一个金额字段来准确记录相关数据。为了帮助用户清楚了解每个支出分类的具体占比和总金额,还需要一个支出分类字段。
最后,考虑到每个报表只能对应一个用户,我们还需要一个字段来标识用户,以确保报表和用户之间的关联关系。
这样设计,既能满足报表统计和分析的需求,也能保证数据结构清晰合理。通过分析,我们得出如下代码:using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using SporeAccounting.BaseModels; namespace SporeAccounting.Models; /// <summary> /// 报表 /// </summary> [Table("Report")] public class Report : BaseModel { /// <summary> /// 年份 /// </summary> [Required] [Column(TypeName = "int")] public int Year { get; set; } /// <summary> /// 月份 /// </summary> [Column(TypeName = "int")] public int? Month { get; set; } /// <summary> /// 季度 /// </summary> [Column(TypeName = "int")] public int? Quarter { get; set; } /// <summary> /// 报表名称 /// </summary> [Required] [Column(TypeName = "nvarchar(100)")] public string Name { get; set; } /// <summary> /// 报表类型 /// </summary> [Required] [Column(TypeName = "int")] public ReportTypeEnum Type { get; set; } /// <summary> /// 金额 /// </summary> [Required] [Column(TypeName = "decimal(18,2)")] public decimal Amount { get; set; } /// <summary> /// 用户Id /// </summary> [Required] [Column(TypeName = "nvarchar(36)")] [ForeignKey("FK_Report_SysUser_UserId")] public string UserId { get; set; } /// <summary> /// 分类Id /// </summary> [Required] [Column(TypeName = "nvarchar(36)")] [ForeignKey("FK_Report_Classification_ClassificationId")] public string ClassificationId { get; set; } /// <summary> /// 导航属性 /// </summary> public SysUser User { get; set; } /// <summary> /// 导航属性 /// </summary> public IncomeExpenditureClassification Classification { get; set; } }
-
功能实现
我们这里只展示和查询报表有关的功能代码,其他代码不再展示。首先,我们要定义数据服务接口IReportServer
,在这个接口中添加获取报表的方法。using SporeAccounting.Models; namespace SporeAccounting.Server.Interface; /// <summary> /// 报表服务 /// </summary> public interface IReportServer { /// <summary> /// 获取报表数据 /// </summary> /// <param name="userId"></param> /// <param name="year"></param> /// <param name="reportType"></param> /// <returns></returns> List<Report> QueryReport(string userId, int year,ReportTypeEnum reportType); }
然后,我们实现这个接口,实现也很简单很简单,我就不具体讲解了。
using SporeAccounting.Models; using SporeAccounting.Server.Interface; namespace SporeAccounting.Server; /// <summary> /// 报表服务实现 /// </summary> public class ReportImp : IReportServer { private readonly SporeAccountingDBContext _sporeAccountingDbContext; public ReportImp(SporeAccountingDBContext sporeAccountingDbContext) { _sporeAccountingDbContext = sporeAccountingDbContext; } /// <summary> /// 获取报表数据 /// </summary> /// <param name="userId"></param> /// <param name="year"></param> /// <returns></returns> public List<Report> QueryReport(string userId, int year, ReportTypeEnum reportType) { try { IQueryable<Report> reports = _sporeAccountingDbContext.Reports .Where(p => p.UserId == userId && p.Year == year && p.Type == reportType); return reports.ToList(); } catch (Exception e) { throw; } } }
最后,我们一起编写Controller。
using System.Net; using AutoMapper; using Microsoft.AspNetCore.Mvc; using SporeAccounting.BaseModels; using SporeAccounting.Models.ViewModels; using SporeAccounting.Server.Interface; namespace SporeAccounting.Controllers { /// <summary> /// 报表控制器 /// </summary> [Route("api/[controller]")] [ApiController] public class ReportController : BaseController { /// <summary> /// 报表服务 /// </summary> private IReportServer _reportServer; /// <summary> /// 映射 /// </summary> private IMapper _mapper; /// <summary> /// 构造函数 /// </summary> /// <param name="reportServer"></param> /// <param name="mapper"></param> public ReportController(IReportServer reportServer, IMapper mapper) { _reportServer = reportServer; _mapper = mapper; } /// <summary> /// 获取报表 /// </summary> /// <param name="report"></param> /// <returns></returns> [HttpPost] [Route("GetReport")] public ActionResult<ResponseData<List<ReportResponseViewModel>>> GetReport([FromBody] ReportViewModel report) { try { string userId = GetUserId(); var reports = _reportServer.QueryReport(userId, report.Year, report.ReportType); List<ReportResponseViewModel> response = _mapper.Map<List<ReportResponseViewModel>>(reports); return Ok(new ResponseData<List<ReportResponseViewModel>>(HttpStatusCode.OK, data: response)); } catch (Exception ex) { return Ok(new ResponseData<bool>(HttpStatusCode.BadRequest, errorMessage: ex.Message)); } } } }
三、总结
本文介绍了记账应用中的报表功能实现,作为单体应用开发的最后环节,其核心是提供一个接口供客户端查询报表数据。本功能实现以清晰的需求分析为起点,逐步完成了模型设计、服务接口定义、接口实现以及控制器编写。需求方面,报表查询需支持按报表类型(包括月报、季报、年报)和时间维度(年份、月份)进行查询,其中月份参数为可选。功能设计中,视图模型定义了报表类型、年份和月份的基本字段,并通过校验属性确保必填字段的正确性。数据库模型进一步扩展了字段设计,涵盖了年份、月份、季度、报表名称、类型、金额、用户关联等内容,满足多维度数据统计和查询需求。服务层通过IReportServer
接口和实现类ReportImp
完成核心数据查询逻辑,利用数据库上下文筛选符合条件的报表数据。最后,控制器ReportController
提供了一个GetReport
接口,负责接收客户端请求并返回查询结果。通过依赖注入服务和自动映射工具,接口实现高效、简洁。整体设计从需求到实现逻辑清晰,功能模块化,便于后续扩展和维护。
在下一篇文章中我们将一起编写报表定时器,实现定时汇总支出数据功能。