HarmonyOS掌上记账APP开发实践第60篇:数据报表与统计 — 按月分组的账单聚合分析
数据报表与统计 — 按月分组的账单聚合分析

文章简介
数据聚合是记账应用后端逻辑的核心能力。MoneyTrack 的统计模块需要将用户的每一笔账单按照月份分组,再按收支类型、分类进行二次聚合,最终以饼图、柱状图、逐日报表等形式呈现。本文聚焦 BillProcessingModel 的数据处理流水线,以及 StatisticsView 中 SummaryType(报表/日历)切换背后的聚合逻辑,帮助开发者掌握鸿蒙应用中的数据统计架构设计,以及大数据量下的性能优化策略。
核心知识点
1. 数据处理流水线
MoneyTrack 的统计数据处理遵循一条清晰的流水线,从原始数据到最终展示经过多个阶段:
这条流水线的每一层都有明确的职责:数据库层负责高效的数据检索,分组层负责将平面数据转化为多层级的统计结构,计算层负责生成图表所需的最终数据格式,渲染层负责视觉呈现。
2. 数据聚合(Aggregation)
BillProcessingModel 负责从本地数据库(AccountingDB)读取指定月份的账单数据,按日期分组为 DailyBillGroupModel[] 列表。每个分组包含 dateStr、list(该日所有账单)等信息。聚合的核心在于将平面化的账单数据转化为多层级的统计结构。
更复杂的 SQL 聚合查询代码:
// 按分类汇总的聚合查询
const categorySummary = dailyBillGroups.map(group => {
const expenseMap = new Map<number, number>();
const incomeMap = new Map<number, number>();
group.list.forEach(bill => {
if (bill.amount < 0) {
// 支出:按资源分类汇总(取绝对值)
const pre = expenseMap.get(bill.resource) ?? 0;
expenseMap.set(bill.resource, pre + Math.abs(bill.amount));
} else {
// 收入:按资源分类汇总
const pre = incomeMap.get(bill.resource) ?? 0;
incomeMap.set(bill.resource, pre + bill.amount);
}
});
return { dateStr: group.dateStr, expenseMap, incomeMap };
});
// 计算月度统计指标
const monthlyStats = {
totalExpense: 0,
totalIncome: 0,
avgDailyExpense: 0,
maxExpenseDay: { date: '', amount: 0 },
categoryRanking: [] as { resource: number; amount: number }[],
};
3. @Computed 在统计中的更多应用
StatisticsVM 中通过 @Computed 计算属性实现了多种维度的聚合,除了基础的饼图、柱状图和报表数据外,还包含更多统计指标:
// 计算日均支出(排除无消费日)
@Computed
public get avgDailyExpense(): number {
const daysWithExpense = this.dailyBillGroups.filter(
g => g.list.some(b => b.amount < 0)
).length;
return daysWithExpense > 0
? this.totalExpense / daysWithExpense
: 0;
}
// 计算各分类占比排名
@Computed
public get categoryRanking(): CategoryRankItem[] {
return Array.from(this.pieChartData)
.sort((a, b) => b.amount - a.amount)
.map((item, index) => ({ ...item, rank: index + 1 }));
}
@Computed 的依赖追踪机制确保:当 dailyBillGroups 发生变化时,所有依赖于它的计算属性都会自动重新求值,无需手动触发更新。
4. 报表导出(Excel/CSV 思路)
MoneyTrack 提供了报表导出功能,支持将月度统计导出为分享图片。除了图片导出,还可以扩展支持 Excel 和 CSV 格式:
- CSV 导出:使用
@ohos.file.fs模块创建文件,将报表数据按行写入逗号分隔的文本格式,适合电子表格软件打开 - Excel 导出:借助三方库生成
.xlsx文件,支持多工作表(按月份分页)和格式化(颜色、边框、公式) - 图片分享:通过
Canvas将报表绘制为图片,调用系统分享组件发送
// CSV 导出思路
function exportToCSV(items: ReportTableItem[]): string {
let csv = '日期,收入,支出,分类\n';
items.forEach(item => {
csv += `${item.date},${item.income},${item.expense},${item.category}\n`;
});
return csv;
}
5. 大数据量下的性能优化
当账单数据量较大时(数月甚至数年的记录),直接在前端进行全量计算可能导致 UI 卡顿。MoneyTrack 采用以下优化策略:
- 分页加载:按月加载数据,切换月份时再加载对应的数据集,避免一次性加载所有历史数据
- Memoization 缓存:对于
@Computed中计算开销较大的聚合操作,利用@Track的缓存机制避免重复计算 - 懒计算:仅在用户切换到统计页面时才触发数据加载和聚合,首页创建账单时不立即触发全量统计重算
- 异步计算:使用
TaskPool将大数据量的聚合计算放到后台线程,避免阻塞 UI 渲染
6. 按月/按分类分组
StatisticsVM 中通过 @Computed 计算属性实现了多种维度的聚合:
- 饼图数据(pieChartData):按资源分类(如餐饮、交通等)汇总金额,使用
Map<number, number>做 key-value 聚合 - 柱状图数据(barChartData):按日期汇总每日收支,生成
BillBarItem[] - 报表数据(reportData):生成逐日的收入/支出对照表,以
ReportTableItem[]表示
@Computed
public get pieChartData(): BillPieChartItem[] {
const map: Map<number, number> = new Map();
// 遍历所有账单,按 resource 分组求和
filteredBills.forEach((item) => {
let preAmount = map.get(item.resource) ?? 0;
map.set(item.resource, preAmount + item.amount);
});
// 转换为 BillPieChartItem[]
}
项目代码案例
BillProcessingModel 处理逻辑
文件路径:bill_data_processing 模块中的 BillProcessingModel
BillProcessingModel 封装了账单调取、分组、统计的核心逻辑,被 HomeVM 和 StatisticsVM 共同持有。它通过 accountProcessing.init() 初始化账户信息,通过 getBillReport() 获取报告数据。
StatisticsView 的 SummaryType 切换
文件路径:features/statistics/src/main/ets/views/StatisticsView.ets
if (this.vm.summaryType === SummaryType.CHART) {
ChartSummary({ vm: this.vm });
} else {
CalendarSummary({ vm: this.vm });
}
SummaryType 枚举定义在 features/statistics/src/main/ets/commons/Enums.ets 中:
export enum SummaryType { CHART, CALENDAR }
推荐参考文档
- ArkTS 数组
reduce/filter/forEach高阶函数 - HarmonyOS @Computed 计算属性文档
- dayjs 日期格式化 API
- HarmonyOS TaskPool 异步任务调度指南
更多推荐



所有评论(0)