数据报表与统计 — 按月分组的账单聚合分析

在这里插入图片描述

文章简介

数据聚合是记账应用后端逻辑的核心能力。MoneyTrack 的统计模块需要将用户的每一笔账单按照月份分组,再按收支类型、分类进行二次聚合,最终以饼图、柱状图、逐日报表等形式呈现。本文聚焦 BillProcessingModel 的数据处理流水线,以及 StatisticsView 中 SummaryType(报表/日历)切换背后的聚合逻辑,帮助开发者掌握鸿蒙应用中的数据统计架构设计,以及大数据量下的性能优化策略。

核心知识点

1. 数据处理流水线

MoneyTrack 的统计数据处理遵循一条清晰的流水线,从原始数据到最终展示经过多个阶段:

渲染错误: Mermaid 渲染失败: Parse error on line 5: ...[按分类二次聚合] D --> E[@Computed 派生统计] ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

这条流水线的每一层都有明确的职责:数据库层负责高效的数据检索,分组层负责将平面数据转化为多层级的统计结构,计算层负责生成图表所需的最终数据格式,渲染层负责视觉呈现。

2. 数据聚合(Aggregation)

BillProcessingModel 负责从本地数据库(AccountingDB)读取指定月份的账单数据,按日期分组为 DailyBillGroupModel[] 列表。每个分组包含 dateStrlist(该日所有账单)等信息。聚合的核心在于将平面化的账单数据转化为多层级的统计结构。

更复杂的 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 封装了账单调取、分组、统计的核心逻辑,被 HomeVMStatisticsVM 共同持有。它通过 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 异步任务调度指南
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐