基于大模型的个人消费分析和理财助手:开发日志 7

月度报告收入统计:收支双维度分析的设计

背景与问题

APP 的月度报告最初只统计支出。这对记账 APP 来说是常见选择——人们记账主要是想控制支出。但事实上,不理解自己的收入结构,就无法评估财务健康状况:"月光的 3000 元花销意味着什么?"如果不对比收入,这句话毫无意义。

改进的目标是让月度报告同时展示收入从哪里来钱花到哪里去,形成完整的财务画像。

后端:拆分为两套独立的统计逻辑

原始代码将所有账单的金额合在一起,没有区分支出和收入:

# 旧方案:按收支方向拆分前
base_conds = [
    Bill.user_id == user.id,
    func.year(Bill.time) == year,
    func.month(Bill.time) == month,
]

改造的关键在于抽象出带方向的查询条件工厂:

def _base_conds(direction: str) -> list:
    return [
        Bill.user_id == user.id,
        func.year(Bill.time) == year,
        func.month(Bill.time) == month,
        Bill.transaction_direction == direction,  # 新增维度筛选
    ]

# 支出统计
exp_conds = _base_conds("支出")
# 收入统计
inc_conds = _base_conds("收入")

核心设计意图: 将原本平行的查询逻辑,按 transaction_direction 拆分为两套独立的统计链路。每条链路都包含:汇总金额、交易笔数、平均每笔金额、分类聚合、每日趋势、最大类别,共计 6 个统计指标。每个指标都同时计算 “outcome_” 和 “income_” 两个版本。

这种设计有意识地和"全量统计再过滤"的方案区分——如果先查全部账单再用 Python 过滤,随着数据量增长会越来越慢。SQLAlchemy 层面就分流,数据库索引能发挥最大作用。

字段命名策略:

class MonthlyReportResponse(BaseModel):
    # 支出
    total_outcome: Decimal
    outcome_count: int
    outcome_avg_per_transaction: Decimal
    outcome_category_breakdown: list[MonthlyReportCategory]
    outcome_daily_trend: list[DailyTrendItem]
    top_outcome_category: MonthlyReportCategory | None
    # 收入
    total_income: Decimal
    income_count: int
    income_avg_per_transaction: Decimal
    income_category_breakdown: list[MonthlyReportCategory]
    income_daily_trend: list[DailyTrendItem]
    top_income_category: MonthlyReportCategory | None

新字段统一加 income_ / outcome_ 前缀,避免与旧字段冲突。旧 API 的 total_outcome 等字段保持不动,保证了前端向后兼容。

前端 UI:双折线趋势图与收入饼图

趋势图的升级:

// chart_view.dart —— 从单一折线改为收支双折线
// 使用两组独立的 LineChartSeries,配合不同颜色和标签
LineChart(
  seriesData: [
    LineChartSeries(
      data: outcomeTrend,  // 红色折线 - 支出
      color: Colors.red.withValues(alpha: 0.6),
    ),
    LineChartSeries(
      data: incomeTrend,   // 绿色折线 - 收入
      color: Colors.green.withValues(alpha: 0.6),
    ),
  ],
  // 图例区分
  legends: [
    LegendEntry(label: "支出", color: Colors.red),
    LegendEntry(label: "收入", color: Colors.green),
  ],
)

双折线放在同一坐标系中,用户可以直观对比某天的收支是否平衡。如果某天支出折线远高于收入折线,可能出现了大额开支。

收入分类饼图:

饼图复用已有的图表组件,但数据源切换为 MonthlyReportCategory(而非原来的 CategorySummaryItem),因为报表 API 返回的格式与首页分类不同。颜色方案选用绿色系,与支出的红色系形成心理上的对比——绿色代表"好的"、红色代表"需要关注的"。

总结

组件 变更内容
analysis_api.py _base_conds(direction) 工厂 + 两套 SQL 查询
back_end/models.py 6 个收入相关新字段
测试 新增 3 笔收入账单(工资/还款/劳务)
front_end/models.dart Dart 反序列化对齐新字段
chart_view.dart 双折线趋势图 + 绿色收入饼图

这次变更的设计哲学是:一个维度的数据不足以做判断。只有支出没有收入,用户看到的是一串"花多少钱"的数字,但无法感知这些钱在自己收入中的占比。当收入饼图出现"工资收入"占比 90% 时,用户意识到自己对单一收入来源的依赖——这是数据驱动的理财意识的第一步。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐