28. 【.NET 8 实战--孢子记账--从单体到微服务】--简易报表--报表定时器与报表数据修正

这篇文章是《.NET 8 实战--孢子记账--从单体到微服务》系列专栏的《单体应用》专栏的最后一片和开发有关的文章。在这片文章中我们一起来实现一个数据统计的功能:报表数据汇总。这个功能为用户查看月度、年度、季度报表提供数据支持。

一、需求

数据统计方面,我们应该考虑一个问题:用户是否需要看到实时数据。一般来说个人记账软件的数据统计是不需要实时的,因此我们可以将数据统计时间设置为每天统计或者每天每月统计,这样我们不仅可以减少统计数据时受到正在写入的数据的影响,也能提升用户体验。在数据更新方面,我们要在每次新增、删除、更新几张记录时进行更新统计报表。整理后的需求如下:

编号 需求 说明
1 统计支出报表 1. 每天定时统计支出数据
2 报表更新 1. 新增、删除、更新支出记录时更新报表数据; 2. 如果报表数据不存在则不进行任何处理

二、功能编写

根据前面的需求,我们分别实现这两个功能。

1. 支出数据统计

因为数据每天都定时更新,因此我们要创建一个定时器来实现这个功能,定时器我们依然使用Quartz来实现。我们在Task\Timer文件夹下新建ReportTimer类来实现定时器。代码如下:

csharp 复制代码
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()创建一个新的服务范围,以便解析依赖项。接着,从服务提供者中获取了三个服务实例:IReportServerIIncomeExpenditureRecordServerIReportLogServer,这些服务分别用于处理报表、支出记录和报表日志。

在代码中,首先查询了所有的报表日志,并按用户分组,以获取每个用户最近一次报表记录的日期。然后,对于每个用户,查询该用户在上次报表日期之后的所有收入和支出记录,并筛选出收入记录。接下来,代码根据这些记录生成季度、年度和月度报表。季度报表按年份和季度分组,年度报表按年份分组,月度报表按年份和月份分组。每个报表包含年份、季度或月份、报表名称、报表类型、金额、用户ID、分类ID、创建日期和创建者ID等信息。生成报表后,代码创建相应的报表日志条目,每个条目包含用户ID、报表ID、创建日期和创建者ID。然后,将这些报表和日志条目添加到数据库中。最后,Execute方法返回一个已完成的任务,表示作业已执行完毕。

核心逻辑是通过定期查询用户的收入和支出记录,生成不同时间维度的财务报表,并将这些报表和相应的日志保存到数据库中。通过实现IJob接口,ReportTimer类可以被Quartz调度器定期触发,从而实现自动化的报表生成和更新。这种设计不仅提高了报表生成的效率,还确保了数据的一致性和完整性。

Tip:这段代码中涉及到了一个新表报表日志,这个用于记录报表数据生成记录的。在这里就不把这个表的结构、操作类列出来了,大家自己动手来实现一下。

2. 报表更新

报表更新逻辑很简单,在这里我们只展示新增的逻辑,其他逻辑大家自己动手实现。我们在IncomeExpenditureRecordImp类的Add方法中添加如下代码:

csharp 复制代码
// 获取包含支出记录记录日期的报表记录
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方法中实现了报表数据的生成和更新逻辑。在实现过程中,通过依赖注入获取必要的服务实例,查询用户的收入和支出记录,生成季度、年度和月度报表,并将这些报表和日志条目保存到数据库中,实现了报表数据的定期更新和持久化存储。此外,还展示了如何在新增支出记录时更新报表数据,确保报表数据的实时性和准确性。通过这种设计,提高了报表生成的效率,确保了数据的一致性和完整性。希望读者能掌握相关技术并应用到实际项目中。

在下一篇文章,也就是这个专栏的最后一篇文章,我们将一起把这个服务端部署到服务器上。

相关推荐
可乐加.糖14 分钟前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s91236010116 分钟前
rust 同时处理多个异步任务
java·数据库·rust
9号达人17 分钟前
java9新特性详解与实践
java·后端·面试
cg501721 分钟前
Spring Boot 的配置文件
java·linux·spring boot
啊喜拔牙28 分钟前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
anlogic1 小时前
Java基础 4.3
java·开发语言
非ban必选1 小时前
spring-ai-alibaba第七章阿里dashscope集成RedisChatMemory实现对话记忆
java·后端·spring
A旧城以西2 小时前
数据结构(JAVA)单向,双向链表
java·开发语言·数据结构·学习·链表·intellij-idea·idea
杉之2 小时前
选择排序笔记
java·算法·排序算法
Naive_72 小时前
蓝桥杯准备(前缀和差分)
java·职场和发展·蓝桥杯