这篇文章是《.NET 8 实战–孢子记账–从单体到微服务》系列专栏的《单体应用》专栏的最后一片和开发有关的文章。在这片文章中我们一起来实现一个数据统计的功能:报表数据汇总。这个功能为用户查看月度、年度、季度报表提供数据支持。
一、需求
数据统计方面,我们应该考虑一个问题:用户是否需要看到实时数据。一般来说个人记账软件的数据统计是不需要实时的,因此我们可以将数据统计时间设置为每天统计或者每天每月统计,这样我们不仅可以减少统计数据时受到正在写入的数据的影响,也能提升用户体验。在数据更新方面,我们要在每次新增、删除、更新几张记录时进行更新统计报表。整理后的需求如下:
编号 | 需求 | 说明 |
---|---|---|
1 | 统计支出报表 | 1. 每天定时统计支出数据 |
2 | 报表更新 | 1. 新增、删除、更新支出记录时更新报表数据; 2. 如果报表数据不存在则不进行任何处理 |
二、功能编写
根据前面的需求,我们分别实现这两个功能。
1. 支出数据统计
因为数据每天都定时更新,因此我们要创建一个定时器来实现这个功能,定时器我们依然使用Quartz
来实现。我们在Task\Timer
文件夹下新建ReportTimer
类来实现定时器。代码如下:
using Quartz;
using SporeAccounting.Models;
using SporeAccounting.Server.Interface;
namespace SporeAccounting.Task.Timer;
/// <summary>
/// 报表定时器
/// </summary>
public class ReportTimer : IJob
{
private readonly IServiceScopeFactory _serviceScopeFactory;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceScopeFactory"></param>
public ReportTimer(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
/// <summary>
/// 执行
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public System.Threading.Tasks.Task Execute(IJobExecutionContext context)
{
using var scope = _serviceScopeFactory.CreateScope();
// 获取每个用户最近一次报表记录日期
var reportServer = scope.ServiceProvider.GetRequiredService<IReportServer>();
var incomeExpenditureRecordServer = scope.ServiceProvider.GetRequiredService<IIncomeExpenditureRecordServer>();
var reportLogServer = scope.ServiceProvider.GetRequiredService<IReportLogServer>();
var reportLogs = reportLogServer.Query();
var reportLogDic = reportLogs
.GroupBy(x => x.UserId)
.ToDictionary(x => x.Key,
x => x.Max(x => x.CreateDateTime));
// 查询上次日期以后的记账记录
List<Report> dbReports = new();
List<ReportLog> dbReportLogs = new();
foreach (var log in reportLogDic)
{
var incomeExpenditureRecords = incomeExpenditureRecordServer
.QueryByUserId(log.Key);
incomeExpenditureRecords = incomeExpenditureRecords
.Where(x => x.RecordDate > log.Value)
.Where(p => p.IncomeExpenditureClassification.Type == IncomeExpenditureTypeEnmu.Income).ToList();
// 生成报表
// 按照季度,年度和月度创建报表数据,将每个人的报表信息写入日志
// 1. 按照季度创建报表数据,根据支出类型统计
var quarterlyReports = incomeExpenditureRecords
.GroupBy(x => new
{
x.RecordDate.Year,
Quarter = (x.RecordDate.Month - 1) / 3 + 1
})
.Select(g => new Report
{
Year = g.Key.Year,
Quarter = g.Key.Quarter,
Name = $"{g.Key.Year}年Q{g.Key.Quarter}报表",
Type = ReportTypeEnum.Quarter,
Amount = g.Sum(x => x.AfterAmount),
UserId = log.Key,
ClassificationId = g.First().IncomeExpenditureClassificationId,
CreateDateTime = DateTime.Now,
CreateUserId = log.Key
}).ToList();
dbReports.AddRange(quarterlyReports);
// 2. 按照年度创建报表数据,根据支出类型统计
var yearlyReports = incomeExpenditureRecords
.GroupBy(x => x.RecordDate.Year)
.Select(g => new Report
{
Year = g.Key,
Name = $"{g.Key}年报表",
Type = ReportTypeEnum.Year,
Amount = g.Sum(x => x.AfterAmount),
UserId = log.Key,
ClassificationId = g.First().IncomeExpenditureClassificationId,
CreateDateTime = DateTime.Now,
CreateUserId = log.Key
}).ToList();
dbReports.AddRange(yearlyReports);
// 3. 按照月度创建报表数据,根据支出类型统计
var monthlyReports = incomeExpenditureRecords
.GroupBy(x => new { x.RecordDate.Year, x.RecordDate.Month })
.Select(g => new Report
{
Year = g.Key.Year,
Month = g.Key.Month,
Name = $"{g.Key.Year}年{g.Key.Month}月报表",
Type = ReportTypeEnum.Month,
Amount = g.Sum(x => x.AfterAmount),
UserId = log.Key,
ClassificationId = g.First().IncomeExpenditureClassificationId,
CreateDateTime = DateTime.Now,
CreateUserId = log.Key
}).ToList();
dbReports.AddRange(monthlyReports);
// 4. 记录日志
var reportLogEntries = dbReports.Select(report => new ReportLog
{
UserId = report.UserId,
ReportId = report.Id,
CreateDateTime = DateTime.Now,
CreateUserId = report.UserId
}).ToList();
dbReportLogs.AddRange(reportLogEntries);
// 保存报表和日志到数据库
reportServer.Add(dbReports);
reportLogServer.Add(dbReportLogs);
}
return System.Threading.Tasks.Task.CompletedTask;
}
}
这段代码定义了一个名为ReportTimer
的类,该类实现了Quartz
库中的IJob
接口。ReportTimer
类的主要功能是根据用户的支出记录定期生成财务报表。首先,代码通过构造函数注入了一个IServiceScopeFactory
实例,用于创建服务范围。在Execute
方法中,使用_serviceScopeFactory.CreateScope()
创建一个新的服务范围,以便解析依赖项。接着,从服务提供者中获取了三个服务实例:IReportServer
、IIncomeExpenditureRecordServer
和IReportLogServer
,这些服务分别用于处理报表、支出记录和报表日志。
在代码中,首先查询了所有的报表日志,并按用户分组,以获取每个用户最近一次报表记录的日期。然后,对于每个用户,查询该用户在上次报表日期之后的所有收入和支出记录,并筛选出收入记录。接下来,代码根据这些记录生成季度、年度和月度报表。季度报表按年份和季度分组,年度报表按年份分组,月度报表按年份和月份分组。每个报表包含年份、季度或月份、报表名称、报表类型、金额、用户ID、分类ID、创建日期和创建者ID等信息。生成报表后,代码创建相应的报表日志条目,每个条目包含用户ID、报表ID、创建日期和创建者ID。然后,将这些报表和日志条目添加到数据库中。最后,Execute
方法返回一个已完成的任务,表示作业已执行完毕。
核心逻辑是通过定期查询用户的收入和支出记录,生成不同时间维度的财务报表,并将这些报表和相应的日志保存到数据库中。通过实现IJob接口,ReportTimer
类可以被Quartz
调度器定期触发,从而实现自动化的报表生成和更新。这种设计不仅提高了报表生成的效率,还确保了数据的一致性和完整性。
Tip:这段代码中涉及到了一个新表报表日志,这个用于记录报表数据生成记录的。在这里就不把这个表的结构、操作类列出来了,大家自己动手来实现一下。
2. 报表更新
报表更新逻辑很简单,在这里我们只展示新增的逻辑,其他逻辑大家自己动手实现。我们在IncomeExpenditureRecordImp
类的Add
方法中添加如下代码:
// 获取包含支出记录记录日期的报表记录
var reports = _sporeAccountingDbContext.Reports
.Where(x => x.UserId == incomeExpenditureRecord.UserId
&& x.Year <= incomeExpenditureRecord.RecordDate.Year &&
x.Month >= incomeExpenditureRecord.RecordDate.Month &&
x.ClassificationId==incomeExpenditureRecord.IncomeExpenditureClassificationId);
// 如果没有就说明程序还未将其写入报表,那么就不做任何处理
for (int i = 0; i < reports.Count(); i++)
{
var report = reports.ElementAt(i);
report.Amount += incomeExpenditureRecord.AfterAmount;
_sporeAccountingDbContext.Reports.Update(report);
}
这段代码添加在了if (classification.Type == IncomeExpenditureTypeEnmu.Income)
分支中,当新增的类型时支出项目时,我们就执行这段代码。在这段代码中,当没有查询到支出记录的话就认为对应该日期的指出记录没有进行数据统计,因此不进行任何处理。
三、总结
在这篇文章中,我们介绍了如何在.NET 8环境下实现定时生成财务报表的功能。首先,分析了需求,确定了报表数据统计的时间和更新策略。然后,通过使用Quartz
库创建了ReportTimer
定时器类,该类实现了IJob
接口,并在其Execute
方法中实现了报表数据的生成和更新逻辑。在实现过程中,通过依赖注入获取必要的服务实例,查询用户的收入和支出记录,生成季度、年度和月度报表,并将这些报表和日志条目保存到数据库中,实现了报表数据的定期更新和持久化存储。此外,还展示了如何在新增支出记录时更新报表数据,确保报表数据的实时性和准确性。通过这种设计,提高了报表生成的效率,确保了数据的一致性和完整性。希望读者能掌握相关技术并应用到实际项目中。
在下一篇文章,也就是这个专栏的最后一篇文章,我们将一起把这个服务端部署到服务器上。