news 2026/9/12 2:14:34

Actual Budget 26.5.0 技术解析:Age of Money 与 Sankey 桑基图报告背后的实现原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Actual Budget 26.5.0 技术解析:Age of Money 与 Sankey 桑基图报告背后的实现原理

Actual Budget 26.5.0 技术解析:Age of Money 与 Sankey 桑基图报告背后的实现原理

【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual

Actual Budget 26.5.0 是 2026 年 5 月发布的一个以「报告能力」为核心的版本,首次引入实验性的 Age of Money(货币账龄)与 Sankey(桑基图)两大新报告,同时带来拆分交易税务式比例分配、BALANCE_OF规则公式、五个全新社区主题以及认证限流等 70 余项特性、修复与工程改进。本文以 26.5.0 发布说明 为主线,结合仓库源码深入剖析这两大新报告的算法与数据流实现,并梳理其余值得关注的增强项,帮助你在升级到 26.5.0 后快速上手新功能,同时理解它们是如何被构建出来的。

版本概览与升级方式

26.5.0 的发布说明将本次更新概括为「强大的新报告能力以及大量修复」,三大核心亮点分别是:

  • 实验性:Age of Money 与 Sankey Diagram 报告;
  • 实验性:自定义主题目录新增五个社区主题(Nord、Ilavenil、Gruvbox Light/Dark、You Need A Theme Light/Dark);
  • 拆分交易中的税务式(比例)分配

官方推荐的部署方式是使用 Docker 镜像,镜像标签为26.5.0

docker pull actualbudget/actual:26.5.0 docker run -d -p 5006:5006 -v actual-data:/data actualbudget/actual:26.5.0

随后发布的 26.5.1 / 26.5.2 为同内容的补丁版本(Docker Tag 26.5.1 / 26.5.2),仅针对认证限流计数、自签名证书处理与 UUID 生成兼容性做了修复,详见后文「补丁版本说明」一节。

需要特别注意的是,Age of Money 与 Sankey 报告目前属于实验性功能:Sankey 需要在「设置 → 实验性功能」中打开Sankey report开关(Experimental.tsx 中的flag="sankeyReport"),Age of Money 同样作为实验性报告进入 Dashboard 组件体系。实验性功能意味着其接口、数据模型与行为仍可能随版本演进发生变化。

亮点一:Age of Money(货币账龄)报告

Age of Money 是预算社区中非常经典的「财务健康度」指标,用于衡量你的钱在进入预算后平均闲置多少天才被花掉。界面中(AgeOfMoney.tsx)给出的官方定义是:

Age of Money 显示你的钱平均在预算中停留多少天后才被消费,它衡量的是「赚到钱」与「花掉钱」之间的时间差。数值越高,说明你在花更早的钱,即「靠上个月的收入生活」而不是「月光」。30 天及以上被认为是理想状态。

计算原理:FIFO 收入桶模型

与许多按"当前余额 / 日均支出"估算的简易算法不同,Actual 的实现采用了严格的FIFO(First In, First Out)模拟:收入按时间排序后形成一个个"收入桶"(income bucket),每次支出按时间顺序从最老的桶里扣减,该笔支出的账龄即「支出日期 − 收入桶日期」。最终展示的数值是最近 10 笔支出的平均账龄

核心逻辑位于 age-of-money-spreadsheet.ts,主要由四个可独立测试的函数组成:

函数职责关键实现细节
classifyTransactions收入/支出分类按金额符号分类:正数(含退款)进入收入池,负数视为支出
calculateAgeOfMoneyFIFO 计算收入排序成桶,支出从最老桶扣减;桶被耗尽则移动到下一个桶;若支出无法被完全覆盖则置insufficientData = true
calculateAverageAge平均账龄取最近 N 笔(默认 10)的年龄求平均并四舍五入
calculateTrend趋势判定比较最近两个数据点,差值超过 ±2 天判定为 ↑/↓,否则 → 稳定

这套实现有几个值得注意的设计决策:

  1. 基于金额而非类别分类:注释明确指出,退款(正数但无收入类别)也应进入资金池,因此采用金额符号判定,而不是依赖类别标记(categoryIsIncome字段被保留备用)。
  2. 全量历史参与 FIFO:收入与支出查询都要求「截至结束日期」的完整历史(amount: { $gt: 0 }/amount: { $lt: 0 }),因为 FIFO 需要完整的收入历史才能正确消耗收入桶——只取报告区间内的数据会算错账龄。
  3. 转账的特殊处理buildTransferInclusionFilter默认排除「预算内账户 ↔ 预算内账户」的转账(它们只是在预算池内移动资金);但当用户对报告施加了账户过滤时,会通过反转账户条件(invertAccountOp)把「对端账户被过滤掉」的预算内转账也纳入收支——例如只筛选支票账户时,信用卡还款应被视为真实支出,报告才能反映"现金账龄"而非全预算账龄。
  4. 分桶粒度:支持daily/weekly/monthly三种粒度(getPeriodKey中周粒度以周一开始),月度粒度按YYYY-MM聚合;日/周粒度下不生成未来日期段,避免滚动均值被"拉平"成水平线。

可视化与状态色

报告页顶部实时展示当前账龄天数与趋势箭头,状态色由 AgeOfMoneyCard.tsx 的getAgeColor定义:

  • 账龄 ≥ 30 天:绿色(理想,reportsNumberPositive);
  • 账龄 ≥ 14 天:黄色(warningText,正在改善);
  • 账龄 < 14 天:红色(reportsNumberNegative,需要努力);
  • 无数据:中性色,显示N/A

趋势箭头基于calculateTrend(±2 天阈值),同时当insufficientData为真时会显示「支出超过同期收入,部分支出无法匹配到收入」的警告。作为 Dashboard 组件,它既可单独进入/reports/age-of-money/:id全屏页面,也可作为卡片(AgeOfMoneyCard)挂载到仪表盘,支持条件过滤(useRuleConditionFilters)、时间范围(滑窗/固定区间)与粒度切换,并可保存为 widget。测试用例见 age-of-money-spreadsheet.test.ts。

亮点二:Sankey 桑基图报告

Sankey 图用「节点 + 流动宽度」直观展示资金的流向:钱从收入端流入,经过账户、类别组,最终流向具体类别。26.5.0 引入的 Sankey 报告(Sankey.tsx、sankey-spreadsheet.ts)是本次版本中工程量最大的新功能,经过 #7582 的数据模型优化后,支持收入来源、图层过滤与更完善的预算处理。

两种视图模式:Spent 与 Budgeted

报告顶部通过GraphModeSelector在两种模式间切换:

  • Spent(已支出):基于真实交易流水,展示「收入来源 → 收入类别 → 账户 → 类别组 → 类别」的资金消耗路径,默认图层范围为 Payee → Category;
  • Budgeted(已预算):基于api/budget-month的预算数据,展示「收入类别 → 可用资金 → 已预算 → 类别组 → 类别」的预算分配路径,默认图层范围为 Income Category → Category。

两种模式的数据来源不同:Spent 模式通过 AQL 查询transactions并按账户、类别分组聚合(fetchCategoryData);Budgeted 模式则逐月请求预算月数据并累加(createBudgetSpreadsheet,对跨月区间逐月求和budgeted/spent/balance/received)。

六层数据模型与图层过滤

整张图由 6 个语义图层(GraphLayers)构成,按固定顺序排列(GRAPH_LAYER_ORDER):

Payee(收入收款方) → IncomeCategory(收入类别) → Account(账户) → Budget(预算) → CategoryGroup(类别组) → Category(类别)
  • Budgeted 模式下不包含 Payee 层;
  • Spent 模式下不包含 Budget 层。

用户可通过工具栏的两个LayerSelector自由选择「From 层」与「To 层」来裁剪视图(如只看 账户 → 类别),normalizeLayerRange保证 from 严格在 to 之前;点击刷新按钮可恢复默认图层范围。

数据构建流水线

Spent 模式的核心查询在fetchCategoryData中按「类别 × 账户(× 收款方)」执行 AQLgroupBy聚合,收入类别的支出还会拆分为正/负两个方向(__NEGATIVE后缀节点处理退款等反向流动)。转账数据(fetchTransferData+aggregateTransferPairs)按transfer_id配对并合并净额。随后buildSankeyData依次执行完整的可视化预处理管线:

  1. cloneGraph深拷贝基础图(避免污染缓存);
  2. groupOtherCategories将 Top N 之外的类别合并进__OTHER_BUCKET节点;
  3. sortGraph按三种排序策略重排节点;
  4. cleanUpNodes清理孤立节点;
  5. addHiddenNodes补齐隐藏节点;
  6. addPercentageLabels计算百分比标签;
  7. addColors基于图表主题着色;
  8. filterGraphByLayers按用户选择的图层区间裁剪;
  9. convertToSankeyData转换为 recharts 的nodes+links数据结构。

预算模式还包含几个特殊节点(SpecialNodeKeys):To budget(待分配资金)、BudgetedOverspent(上月超支)、For next month(预留下月)、From previous month(上月结转)与Available income(可用资金),完整还原预算工作流的资金语义;超额预算(toBudget < 0)时显示为「Overbudgeted」反向流。

交互与可配置项

Sankey 报告的工具栏提供了相当完整的配置能力:

配置项选项 / 默认值说明
视图模式Spent / Budgeted,默认 Spent切换交易流与预算流
Top N 类别All(1e5 哨兵值)/10/15/20/25/30,默认 15超过上限的类别并入 Other 桶;同时受卡片高度限制topNNodes = max(2, floor(cardHeight/35px))
类别排序Sort per group / Sort all / Sort as budget三种排序策略
图层范围From/To 六层自由组合见上文
Options 菜单百分比显示 / Spent 视图分组账户 / Spent 视图显示转账均为布尔开关(showPercentagesgroupAccountsshowTransfers

groupAccounts开启后所有账户聚合为单个「Income」节点(SpecialNodeKeys.AllAccounts);showTransfers则会在账户节点之间绘制转账连线。该报告同样支持条件过滤器与时间范围(含 1 个月快捷选项),并可作为 Dashboard widget 保存(meta 中持久化modetopNcategoriescategorySortlayerFromlayerTo等全部配置)。后续修复还保证了 #7619「Sankey 卡片遵循报告设置」与 #7632「收款方为空时收入不被误判为支出」。

亮点三:拆分交易的税务式比例分配

26.5.0 为拆分交易新增了「按比例分配剩余金额」的选项(#7257)。此前拆分交易只能把差额平均分配给空白的子分录,而现在支持按各子分录的既有金额比例分摊差额——这正是税务场景(如按比例分摊税费)所需要的分配方式。

实现位于 TransactionsTable.tsx 的分配处理器中,分两条路径:

路径一:存在空白子分录(amount === 0)时等额分配先计算差额remainingAmount = parentAmount - Σ(sibling amounts),再Math.floor(remaining / 空子分录数)得到基础份额,多余的"分"(remainingCents)逐笔 +1 分配,保证金额总和精确等于父交易金额,不产生一分钱误差。

路径二:无空白子分录时按比例分配按现有子分录的占比计算份额:newAmount = floor(siblingAmount / ΣsiblingAmounts × remainingAmount) + siblingAmount,随后用一个循环把所有子分录之和修正到恰好等于父交易金额(多则逐笔 -1、少则逐笔 +1,循环取模分配)。

这种「先比例、后余数修正」的策略确保了任意金额组合下拆分总和都与父交易严格相等,且分配的优先级是等额优先于比例。

其他值得关注的增强项

规则公式新增BALANCE_OF

#7335 为规则与报告公式新增BALANCE_OF(account_id_or_name)函数,用于读取其他账户的余额。根据 formulaCatalog.ts 的注册信息:

  • 支持querytransaction两种模式;
  • 规则公式中,返回该笔交易发生时刻的运行余额(单位:分),与当前账户的balance变量口径一致;
  • 报告/查询公式中,返回该账户的当前余额(货币显示单位)
  • 参数为带引号的账户 ID(确定性匹配)或精确账户名。

测试见 useFormulaExecution.test.ts:按 ID 解析并查询余额、按精确名称解析、未知账户返回 0 且不发查询。实现层面通过balanceOfNames预取映射(useFormulaExecution.ts)批量解析公式中的账户引用。

自定义主题:五个新社区主题与持久化 CSS 覆盖

自定义主题目录(Custom Themes)在 26.5.0 一次性新增五个社区主题:#7513 Nord、#7543 Ilavenil、#7571 Gruvbox Light/Dark、#7441/#7447 You Need A Theme Light/Dark(后者基于 2026 nYNAB 配色)。同时 #7495 修复了一个长期痛点:自定义 CSS 覆盖现在可以在主题切换后持久保留,且在主题下拉菜单旁边会显示可见的激活指示器;#7253 改善了主题目录的响应式表现。工程侧还增加了 #7566「每晚自动扫描主题目录以发现损坏主题」的 CI 保障。

认证端点限流(防暴力破解)

#7432 为认证端点引入了速率限制,防止暴力破解攻击。值得留意的是补丁版本 #7707 将其修正为「仅统计失败的登录尝试」计入限流,避免合法用户因多次输错密码被误锁的同时,也避免正常登录流量触发误伤——这正是发布说明中强调的「认证限流」修复项。

其他增强速览

  • 预算分析报告支持类别组(#7116);
  • 自定义报告 Live 模式新增 "Last 30 days" 时间范围(#7217),对应 ReportOptions.ts 中的last30Days选项;
  • 每笔计划单独覆盖 "upcoming" 通知窗口(#7434):允许每个排程(schedule)拥有自己的提醒时间窗,而不必受全局设置约束;
  • Dashboard 组件错误边界(#7382)与规则页 scoped ErrorBoundary(#7437):组件级渲染崩溃不再拖垮整个应用;
  • API 类型导出(#7581):可通过@actual-app/api/models直接导入模型类型;#7468 同步为@actual-app/core发布.d.ts声明文件,修复严格 TypeScript 下游的类型错误;
  • 双击 Ctrl-f 触发浏览器查找(#7605);
  • Reimport Deleted 默认开启并在导入间保持状态(#7610);
  • 智利比索 CLP 货币支持(#7346);
  • 对账表单排版重构(#7423),提升清晰度与易用性;
  • CLI 改进(#7378):账户排序、更完善的 Agent 使用说明与类型修复;
  • 端到端加密说明澄清(#7392):E2E 加密仅保护预算数据,不包含银行同步令牌;同时 #7368 允许桌面端对预算文件启用端到端加密。

关键 Bugfix 盘点

26.5.0 修复了一批影响日常使用的缺陷,按主题可归纳为:

  • API 与数据完整性:#7242 修复api.updateTransaction()部分更新时损坏拆分父交易的问题;#7453 修复子拆分交易的解锁异常。
  • 搜索与过滤:#7270 修复快捷搜索误将?%当作通配符导致返回全部交易的问题;#7324 修复以$开头的标签查询失败;#7304 修复切换过滤操作符时显示 UUID。
  • 安全:#7428 修复文件上传净化中的路径遍历漏洞;#7608 禁止初始化后重新配置 OpenID。
  • 报告:#7296 修复净资产图时间间隔小于设定值;#7356 修复自定义报告编辑器在路由切换间保留未保存设置的问题;#7619/#7632 见上文 Sankey 修复。
  • 稳定性:#7381 修复过期循环排程导致账本页崩溃;#7532 修复清空交易收款方报错;#7623 修复余额模板金额不可整除时的无限循环;#7564 修复 Docker 因未解析的工作区依赖而无法启动;#7656 修复标签页挂起后共享 Worker 恢复。
  • 一致性:#7496 修复 Web 与移动端在未分类交易菜单上「转账」入口不一致的问题;#7572 修复交易行拖拽干扰行内文本编辑。

维护与工程化改进

发布说明中的 Maintenance 条目反映了这个版本在工程质量上的投入,值得工程团队借鉴:

  • 发布自动化升级(#7407/#7408/#7418):发布说明直接生成文档页面,并取消每月合并冻结的流程;#7635/#7640 修复冲突与 cherry-pick 提交下的说明生成脚本;
  • 模块化改造(#7429/#7446/#7462):loot-core 内部导入迁移至 Node.js subpath imports(#server/*#shared/*),所有包统一模块导入规范,并新增 #7467 架构边界 ESLint 规则;
  • CI 加固(#7433/#7448/#7465/#7533):修复 GitHub Actions 中的脚本注入模式、引入zizmor检查与自动修复,减少 stale 工作流权限;
  • 发布安全(#7556/#7579/#7583):nightly 与正式 npm 包启用 Trusted Publishing,并统一发布工作流;
  • 构建效率(#7503/#7551):e2e 测试改用预构建包、共享 CI 依赖安装步骤以削减yarn install次数;
  • 工具链升级(#7463/#7508):oxlint/oxfmt 升级、ESLint v10;#7538 关闭产物压缩以保留可读的生产报错堆栈;#7547 关闭 Electron 构建矩阵 fail-fast。

补丁版本说明:26.5.1 / 26.5.2

紧随其后的 26.5.1 与 26.5.2 为功能完全一致的补丁版本(多出的 26.5.2 仅为解决 Windows Store 发布问题),核心修复见 26.5.1 发布说明:

  • #7707认证限流仅统计失败登录尝试(见上文);
  • #7713修复桌面应用自签名证书功能;
  • #7734UUID 生成回退到uuid库(而非crypto.randomUUID()),以兼容 HTTP 非安全上下文环境。

如果你的部署环境使用了自签名证书,或通过 HTTP 访问(非 localhost),建议升级到 26.5.1 及以上版本。

结语

从实现层面看,26.5.0 的两个新报告体现了 Actual 报告体系的两个方向:Age of Money 用可验证的 FIFO 算法提供「财务健康度」的量化指标,Sankey 则以六层数据模型 + 可配置管线提供资金流向的可视化洞察。二者都深度复用了规则条件过滤(useRuleConditionFilters)、时间范围与 Dashboard widget 机制,使得报告不再是孤立的页面,而是可以组合、过滤、保存的"一等公民"。配合认证限流、E2E 加密澄清、主题持久化等增强,26.5.0 是升级价值相当高的一次发布——如果对上述源码细节感兴趣,建议继续阅读 age-of-money-spreadsheet.ts 与 sankey-spreadsheet.ts 的完整实现及配套测试。

【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 2:13:44

RD-Agent Web UI 日志卡顿优化指南:10 分钟从卡死到流畅

RD-Agent Web UI 日志卡顿优化指南&#xff1a;10 分钟从卡死到流畅 【免费下载链接】RD-Agent Research and development (R&D) is crucial for the enhancement of industrial productivity, especially in the AI era, where the core aspects of R&D are mainly fo…

作者头像 李华
网站建设 2026/9/12 2:11:16

基于主从博弈的售电商多元零售套餐与多级市场购电策略复现

做电力市场方向的算法复现&#xff0c;最怕遇到两类题目&#xff1a;一类是模型稀碎&#xff0c;论文里全是“显然可得”&#xff0c;代码根本对不上&#xff1b;另一类是模型完整&#xff0c;但求解器一跑就是十几个小时&#xff0c;收敛了算你运气好。这个“基于主从博弈的售…

作者头像 李华
网站建设 2026/9/12 2:05:57

Windows部署vLLM服务Qwen3-8B-FP8:WSL2与Docker双方案实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:03:37

孪生神经网络在点选识别中的实战应用

简介&#xff1a;本资源是一套基于孪生神经网络&#xff08;Siamese Network&#xff09;实现的点选验证码识别完整项目&#xff0c;面向人工智能、计算机科学、自动化等专业的在校学生、教师及初入CV领域的开发者&#xff0c;解决图像匹配与小样本识别场景下的点选交互式验证码…

作者头像 李华