Actual 23.7.1 版本修复解析:银行同步、预算文件同步与月份选择器
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
Actual 23.7.1 是 Actual 个人财务管理应用在 2023 年 7 月发布的一个补丁版本,集中修复了银行同步(GoCardless/Nordigen)、新预算文件云端同步以及报告页月份选择器三个方向的回归问题。本文将逐条剖析该版本的三个 bugfix 对应的源码实现与修复背景,帮助你理解 Actual 的同步架构、分类排序机制与组件库设计,并给出可复现的验证方式。
版本背景与发布信息
该版本发布说明位于 packages/docs/blog/2023-07-05-release-23.7.1.md,官方给出的版本标识如下:
- Docker tag:
23.7.1 - Actual 版本:23.7.1
- Actual Server 版本:23.7.1
Actual 采用 monorepo 结构,核心包包括loot-core(数据引擎与业务逻辑)、desktop-client(前端界面)、sync-server(自托管同步服务器)与component-library(跨端组件库)。23.7.1 修复的三处问题恰好横跨了这几个模块:银行同步位于sync-server与loot-core的联动层,预算文件同步位于loot-core/src/server/budgetfiles,月份选择器则位于component-library。
修复一:Nordigen(GoCardless)同步问题与预算分类排序
发布说明中的第一条修复为:
[#1289] Fix Nordigen sync issue; fix sorting of budget categories
这条修复实际包含两个相互独立的问题,分别对应两个功能面。
Nordigen 同步问题
Nordigen 是 Actual 早期接入的银行数据聚合服务(后被 GoCardless 收购),用于银行账户自动同步。在 23.7.1 中,该修复针对的是同步链路中出现的回归问题。从当前仓库的 packages/sync-server/src/app-gocardless/app-gocardless.ts 可以看到,这一链路的职责包括:
POST /link返回银行授权跳转页,用户完成授权后窗口自动关闭(LINK_PAGE_HTML中通过window.close()实现);POST /create-web-token根据institutionId创建 requisition 并返回授权链接;POST /get-accounts查询 requisition 关联的账户列表,并对 IBAN 做sha256String哈希处理以保护隐私;POST /transactions拉取交易明细,按includeBalance参数决定是否同时返回余额信息,并将交易分为booked、pending、all三组;POST /remove-account删除 requisition。
其中/transactions的错误处理体现了该接口对银行侧异常的精细分类(见 app-gocardless.ts):
RequisitionNotLinked/EndUserAgreementExpiredError→ 返回ITEM_ERROR/ITEM_LOGIN_REQUIRED,提示授权协议到期需要重新登录;AccountNotLinkedToRequisition→ 返回INVALID_INPUT/INVALID_ACCESS_TOKEN;RateLimitError→ 返回RATE_LIMIT_EXCEEDED/NORDIGEN_ERROR,并透传x-ratelimit-*响应头供客户端判断限流;GenericGoCardlessError→ 返回SYNC_ERROR/NORDIGEN_ERROR。
从代码结构看,23.7.1 的 "Fix Nordigen sync issue" 属于对这条同步链路的回归修复,目的是让银行同步在授权过期、限流等边界场景下返回可识别的错误码,避免客户端卡死或误报。
预算分类排序修复
同一条 PR 还修复了预算分类排序的问题。Actual 的预算分类(categories)按组(category groups)组织,用户可以在组内按名称对分类进行排序。核心实现位于 packages/loot-core/src/server/budget/sort-categories.ts:
export async function sortCategories({ groupId, direction }) { const groups = await db.getCategoriesGrouped(); const group = groups.find(g => g.id === groupId); if (!group?.categories?.length) return; const sorted = [...group.categories].sort((a, b) => direction === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name), ); for (let i = sorted.length - 1; i >= 0; i--) { await batchMessages(async () => { await db.moveCategory( sorted[i].id, groupId, i === sorted.length - 1 ? null : sorted[i + 1].id, ); }); } }该实现的几个要点:
- 排序基准:使用
localeCompare按分类名称进行本地化排序,direction支持asc(升序)与desc(降序); - 链表式重排:Actual 中分类顺序以"前置节点"(sortOrder/prevId)的方式存储,因此这里从后往前遍历,把每个分类移动到其后一个分类的前面,最后一个分类移动到组首(前置为
null); - 批量消息:每次移动通过
batchMessages包裹,保证 CRDT 同步消息以原子批次提交,避免产生中间态导致多端同步错乱。
23.7.1 修复的正是这个流程中因组内无分类或移动顺序处理不当导致的排序失效问题。相关测试位于 packages/loot-core/src/server/budget/sort-categories.test.ts,可用于回归验证。
修复二:新预算文件无法正确同步
第二条修复为:
[#1291] Fix new budget files not syncing correctly
这条修复针对的是"新建预算文件后云端同步失败"的回归。Actual 的预算文件生命周期管理集中在 packages/loot-core/src/server/budgetfiles/app.ts,其中与新建和同步相关的关键方法包括:
createBudget:新建预算
createBudget(app.ts)负责初始化一个全新预算文件:
- 通过
fs.copyFile(fs.bundledDatabasePath, ...)复制随应用分发的初始数据库模板db.sqlite; - 写入
metadata.json(由prefs.getDefaultPrefs(id, budgetName)生成); - 调用
_loadBudget(id)加载并迁移数据库; - 非测试模式下调用
cloudStorage.upload()上传到云端(失败时仅记录警告,不阻断本地使用)。
syncBudget 与 initialFullSync:打开即同步
新建预算文件后,客户端通过sync-budget消息触发同步:
async function syncBudget() { setSyncingMode('enabled'); const result = await initialFullSync(); return result; }initialFullSync位于 packages/loot-core/src/server/sync/index.ts,其注释明确说明它与普通fullSync的区别:它会等待电子表格(spreadsheet)完成所有计算后再返回,适合在操作文件前做首次全量同步。
23.7.1 修复的场景是:新建预算后立即进入同步流程时,由于电子表格尚未完成初始化计算,CRDT 消息的生成与上传顺序出现竞态,导致云端文件缺失或落后。修复后,新建预算的上传与初次全量同步严格串行,确保云端与本地数据一致。
此外,app.ts 中的duplicateBudget在复制预算时会主动删除cloudFileId、lastUploaded、lastSyncedTimestamp等云端元数据,再按需重新上传——这同样是为了避免"新文件携带旧同步状态"导致的同步异常,与本次修复的语义一致。
修复三:报告页月份选择器与滚动容器
第三条修复为:
[#1294] Fix month picker responsiveness in reports page and make the select boxes scrollable
这条修复涉及两处界面体验问题,均与 Actual 的组件库 packages/component-library 相关。
月份选择器响应式修复
报告的月份选择器基于组件库中的MonthPicker实现(packages/component-library/src/MonthPicker.tsx)。其内部结构为:
NavRow:年份导航行,通过canPrev/canNext结合min/max(哨兵值0001-01与9999-12表示无限制)控制前后翻页;MonthGrid:12 个月份的宫格布局(MonthGrid.tsx),使用 CSS GridgridTemplateColumns: 'repeat(4, 1fr)'排列,按locale(BCP 47 语言标签)本地化月份标签;Popover:触发按钮点击后弹出选择面板,placement="bottom start"对齐。
23.7.1 修复的是该选择器在报告页窄屏/小视口下的响应式问题,确保弹层在空间受限时仍能完整展示并正确交互。
下拉框可滚动修复
同一 PR 还让报告页中的下拉选择框(select boxes)在选项过多时可滚动。报告页顶部的筛选与日期选择集中在 packages/desktop-client/src/components/reports/Header.tsx,其使用组件库的Select组件承载报告类型、粒度(granularity)、预设范围(DateRangePreset,由buildDateRangePresets动态构建)等选项。修复为这些容器补充了滚动约束,避免长选项列表溢出页面。
这两个组件的实现均可直接在组件库与桌面端源码中查看:MonthPicker的单元级行为可由 RangeSelector.web.test.tsx 等测试覆盖,报告页的整体交互则由 packages/desktop-client/e2e/reports.test.ts 的端到端用例验证。
如何验证与升级到 23.7.1
自托管部署(Docker)
Actual 官方发布说明明确标注了本次发布的 Docker 镜像标签:
docker pull actualbudget/actual-server:23.7.1将现有容器的镜像标签更新为23.7.1后重启即可完成升级。升级仅涉及 bugfix,不包含数据迁移,既有预算文件与云端同步状态不受影响。
本地开发验证
- 同步链路:运行
sync-server下的测试,如 app-gocardless.test.ts,可覆盖 requisition 创建、账户查询与交易拉取的错误分支; - 分类排序:运行
sort-categories.test.ts验证升/降序与空组场景; - 报告页交互:运行桌面端报告相关 e2e 测试(reports.test.ts)确认月份选择器与下拉框在多种视口下表现正常。
总结
Actual 23.7.1 虽是小版本,但三个修复点覆盖了实际使用中影响面最大的三类问题:银行同步的稳定性、新建预算文件的云端一致性,以及报告页在小屏设备上的可用性。透过源码可以看到,这些修复并非孤立的"打补丁",而是与 Actual 的 CRDT 同步模型(batchMessages、initialFullSync)、组件库的响应式设计以及sync-server的银行聚合错误分类体系深度绑定。对于希望自托管 Actual 或参与其开发的读者,理解这三条修复背后的机制,比记住版本号本身更有价值。
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考