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

这篇文章是《.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()创建一个新的服务范围,以便解析依赖项。接着,从服务提供者中获取了三个服务实例:IReportServerIIncomeExpenditureRecordServerIReportLogServer,这些服务分别用于处理报表、支出记录和报表日志。
在代码中,首先查询了所有的报表日志,并按用户分组,以获取每个用户最近一次报表记录的日期。然后,对于每个用户,查询该用户在上次报表日期之后的所有收入和支出记录,并筛选出收入记录。接下来,代码根据这些记录生成季度、年度和月度报表。季度报表按年份和季度分组,年度报表按年份分组,月度报表按年份和月份分组。每个报表包含年份、季度或月份、报表名称、报表类型、金额、用户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方法中实现了报表数据的生成和更新逻辑。在实现过程中,通过依赖注入获取必要的服务实例,查询用户的收入和支出记录,生成季度、年度和月度报表,并将这些报表和日志条目保存到数据库中,实现了报表数据的定期更新和持久化存储。此外,还展示了如何在新增支出记录时更新报表数据,确保报表数据的实时性和准确性。通过这种设计,提高了报表生成的效率,确保了数据的一致性和完整性。希望读者能掌握相关技术并应用到实际项目中。
在下一篇文章,也就是这个专栏的最后一篇文章,我们将一起把这个服务端部署到服务器上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/69449.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入探索C++17的std::any:类型擦除与泛型编程的利器

文章目录 基本概念构建方式构造函数直接赋值std::make_anystd::in_place_type 访问值值转换引用转换指针转换 修改器emplaceresetswap 观察器has_valuetype 使用场景动态类型的API设计类型安全的容器简化类型擦除实现 性能考虑动态内存分配类型转换和异常处理 总结 在C17的标准…

物管系统赋能智慧物业管理提升服务质量与工作效率的新风潮

内容概要 在当今的物业管理领域&#xff0c;物管系统的崛起为智慧物业管理带来了新的机遇和挑战。这些先进的系统能够有效整合各类信息&#xff0c;促进数字化管理&#xff0c;从而提升服务质量和工作效率。通过物管系统&#xff0c;物业管理者可以实时查看和分析各种数据&…

分组表格antd+ react +ts

import React from "react"; import { Table, Tag } from "antd"; import styles from "./index.less"; import GroupTag from "../Tag"; const GroupTable () > {const columns [{title: "姓名",dataIndex: "nam…

【JAVA实战】如何使用 Apache POI 在 Java 中写入 Excel 文件

大家好&#xff01;&#x1f31f; 在这篇文章中&#xff0c;我们将带你深入学习如何使用 Apache POI 在 Java 中编写 Excel 文件的技巧&#xff01;&#x1f4ca;&#x1f4da; 如果你是 Java 开发者&#xff0c;或者正在探索如何处理 Excel 文件的数据&#xff0c;那么这篇文章…

使用Avalonia UI实现DataGrid

1.Avalonia中的DataGrid的使用 DataGrid 是客户端 UI 中一个非常重要的控件。在 Avalonia 中&#xff0c;DataGrid 是一个独立的包 Avalonia.Controls.DataGrid&#xff0c;因此需要单独通过 NuGet 安装。接下来&#xff0c;将介绍如何安装和使用 DataGrid 控件。 2.安装 Dat…

C#分页思路:双列表数据组合返回设计思路

一、应用场景 需要分页查询&#xff08;并非全表查载入物理内存再筛选&#xff09;&#xff0c;返回列表1和列表2叠加的数据时 二、实现方式 列表1必查&#xff0c;列表2根据列表1的查询结果决定列表2的分页查询参数 三、示意图及其实现代码 1.示意图 黄色代表list1的数据&a…

【Linux】磁盘

没有被打开的文件 文件在磁盘中的存储 认识磁盘 磁盘的存储构成 磁盘的效率 与磁头运动频率有关。 磁盘的逻辑结构 把一面展开成线性。 通过扇区的下标编号可以推算出在磁盘的位置。 磁盘的寄存器 控制寄存器&#xff1a;负责告诉磁盘是读还是写。 数据寄存器&#xff1a;给…

第13章 深入volatile关键字(Java高并发编程详解:多线程与系统设计)

1.并发编程的三个重要特性 并发编程有三个至关重要的特性&#xff0c;分别是原子性、有序性和可见性 1.1 原子性 所谓原子性是指在一次的操作或者多次操作中&#xff0c;要么所有的操作全部都得到了执行并 且不会受到任何因素的干扰而中断&#xff0c;要么所有的操作都不执行…

记录 | Docker的windows版安装

目录 前言一、1.1 打开“启用或关闭Windows功能”1.2 安装“WSL”方式1&#xff1a;命令行下载方式2&#xff1a;离线包下载 二、Docker Desktop更新时间 前言 参考文章&#xff1a;Windows Subsystem for Linux——解决WSL更新速度慢的方案 参考视频&#xff1a;一个视频解决D…

stack 和 queue容器的介绍和使用

1.stack的介绍 1.1stack容器的介绍 stack容器的基本特征和功能我们在数据结构篇就已经详细介绍了&#xff0c;还不了解的uu&#xff0c; 可以移步去看这篇博客哟&#xff1a; 数据结构-栈数据结构-队列 简单回顾一下&#xff0c;重要的概念其实就是后进先出&#xff0c;栈在…

JUC--ConcurrentHashMap底层原理

ConcurrentHashMap底层原理 ConcurrentHashMapJDK1.7底层结构线程安全底层具体实现 JDK1.8底层结构线程安全底层具体实现 总结JDK 1.7 和 JDK 1.8实现有什么不同&#xff1f;ConcurrentHashMap 中的 CAS 应用 ConcurrentHashMap ConcurrentHashMap 是一种线程安全的高效Map集合…

C++17 std::variant 详解:概念、用法和实现细节

文章目录 简介基本概念定义和使用std::variant与传统联合体union的区别 多类型值存储示例初始化修改判断variant中对应类型是否有值获取std::variant中的值获取当前使用的type在variant声明中的索引 访问std::variant中的值使用std::get使用std::get_if 错误处理和访问未初始化…

NLP自然语言处理通识

目录 ELMO 一、ELMo的核心设计理念 1. 静态词向量的局限性 2. 动态上下文嵌入的核心思想 3. 层次化特征提取 1. 双向语言模型&#xff08;BiLM&#xff09; 2. 多层LSTM的层次化表示 三、ELMo的运行过程 1. 预训练阶段 2. 下游任务微调 四、ELMo的突破与局限性 1. 技术突破 2. …

在做题中学习(82):最小覆盖子串

解法&#xff1a;同向双指针——>滑动窗口 思路&#xff1a;题目要求找到s里包含t所有字符的最小子串&#xff0c;这就需要记录在s中每次查找并扩大范围时所包含进去的字符种类是否和t的相同&#xff0c;并且&#xff1a;题目提示t中会有重复字符&#xff0c;因此不能简单认…

【deepseek】deepseek-r1本地部署-第二步:huggingface.co替换为hf-mirror.com国内镜像

一、背景 由于国际镜像国内无法直接访问&#xff0c;会导致搜索模型时加载失败&#xff0c;如下&#xff1a; 因此需将国际地址替换为国内镜像地址。 二、操作 1、使用vscode打开下载路径 2、全局地址替换 关键字 huggingface.co 替换为 hf-mirror.com 注意&#xff1a;务…

DeepSeek:突破传统的AI算法与下载排行分析

DeepSeek的AI算法突破DeepSeek相较于OpenAI以及其它平台的性能对比DeepSeek的下载排行分析&#xff08;截止2025/1/28 AI人工智能相关DeepSeek甚至一度被推上了搜索&#xff09;未来发展趋势总结 在人工智能技术飞速发展的当下&#xff0c;搜索引擎市场也迎来了新的变革。DeepS…

java 判断Date是上午还是下午

我要用Java生成表格统计信息&#xff0c;如下图所示&#xff1a; 所以就诞生了本文的内容。 在 Java 里&#xff0c;判断 Date 对象代表的时间是上午还是下午有多种方式&#xff0c;下面为你详细介绍不同的实现方法。 方式一&#xff1a;使用 java.util.Calendar Calendar 类…

【Matlab高端绘图SCI绘图模板】第05期 绘制高阶折线图

1.折线图简介 折线图是一个由点和线组成的统计图表&#xff0c;常用来表示数值随连续时间间隔或有序类别的变化。在折线图中&#xff0c;x 轴通常用作连续时间间隔或有序类别&#xff08;比如阶段1&#xff0c;阶段2&#xff0c;阶段3&#xff09;。y 轴用于量化的数据&#x…

【Java数据结构】了解排序相关算法

基数排序 基数排序是桶排序的扩展&#xff0c;本质是将整数按位切割成不同的数字&#xff0c;然后按每个位数分别比较最后比一位较下来的顺序就是所有数的大小顺序。 先对数组中每个数的个位比大小排序然后按照队列先进先出的顺序分别拿出数据再将拿出的数据分别对十位百位千位…

Linux的常用指令的用法

目录 Linux下基本指令 whoami ls指令&#xff1a; 文件&#xff1a; touch clear pwd cd mkdir rmdir指令 && rm 指令 man指令 cp mv cat more less head tail 管道和重定向 1. 重定向&#xff08;Redirection&#xff09; 2. 管道&#xff08;Pipes&a…