Actual 接入 GoCardless 银行同步完整指南:密钥配置、账户关联与同步机制解析
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
Actual 是一款本地优先(local-first)的个人财务管理应用,内置了基于 GoCardless(原 Nordigen)开放银行(Open Banking)协议的银行同步能力。本文以 Actual 官方文档为核心,结合仓库内
sync-server与desktop-client的实际实现,完整讲解从 GoCardless 开发者后台申请密钥、在 Actual 中完成服务端配置、将现有或新建账户与银行账户关联,以及同步背后的端到端调用链路与限额说明。读完本文,你将能够独立完成 Actual 的 GoCardless 银行同步配置,并理解其"服务端托管密钥 + Requisition 授权 + 银行适配器归一化"的底层工作原理。
:::note 本功能要求Client(客户端)版本 23.7.0 及以上、Server(服务端)版本 23.7.0 及以上。请确认你部署的 Actual 客户端与服务端均满足该版本条件后再进行配置。 :::
:::warning 从2025 年 7 月起,GoCardless 已停止接受新的Bank Account Data账户注册。下文中的注册指引已过时,但如果你已是存量用户,现有账户仍可继续使用,不受影响。 :::
一、GoCardless 银行同步在 Actual 中的定位与架构
在进入操作步骤之前,先明确 GoCardless 在 Actual 中的角色。从源码结构看,GoCardless 是 Actual 五大银行同步提供商(goCardless、simpleFin、pluggyai、enableBanking、akahu)之一,该列表定义于 bank-sync.ts 的SYNC_PROVIDERS常量中。
整个同步链路分为三层:
- 桌面客户端(desktop-client):负责 UI 交互——引导用户输入密钥、选择银行机构、展示授权跳转与账户选择;相关实现见 gocardless.ts。
- 同步服务端(sync-server):托管 GoCardless 密钥,向 GoCardless Bank Account Data API 发起请求,并对各银行返回的原始数据进行归一化;核心实现位于 app-gocardless.ts 与 gocardless-service.ts。
- 核心引擎(loot-core):通过
GOCARDLESS_SERVER配置项将客户端与服务端地址串接起来,见 server-config.ts。
客户端与服务端之间的通信通过一系列 RPC 方法完成,例如gocardless-create-web-token(创建 Requisition 并返回授权链接)、gocardless-poll-web-token(轮询授权状态)、get-banks、get-accounts、transactions等,均可在 app-gocardless.ts 中找到对应的 HTTP 路由实现。客户端在授权成功后,会弹出select-linked-accounts模态框让用户选择要同步的账户。
二、为 Actual 创建 GoCardless SECRET 与 KEY
2.1 版本前提
在开始之前,再次确认你的 Actual 部署满足:
- Client 版本 ≥ 23.7.0
- Server 版本 ≥ 23.7.0
否则界面中不会出现 GoCardless 相关入口,或同步功能无法正常工作。
2.2 创建步骤
前往 GoCardless 开放银行开发者门户创建账户:
https://bankaccountdata.gocardless.com/overview/(对应 Bank Account Data API 服务)。登录账户后台,在左侧菜单选择Developers → User secrets。
点击左下角'+ create new'按钮创建新的用户密钥。
- 务必下载你的密钥文件,因为key(密钥本身)将不会在账户后台再次显示,丢失后只能重新创建;
- 这对
secret_id/secret_key将用于 Actual 建立银行同步连接。
为密钥输入一个便于识别的名称,然后点击 Create。该名称仅用于你在 GoCardless User secrets 概览页中快速区分不同密钥,不影响 Actual 侧的任何行为。
下载生成的密钥文件并妥善保存在本地电脑。
回到 Actual,点击侧边栏底部的"+ Add account"。
在弹出的对话框中选择"Set-up GoCardless for bank-sync"。
按提示输入你的 GoCardlesssecret ID与secret key。这些值会保存在服务端,因此只需输入一次,之后所有账户的同步都会复用这套凭据。
2.3 密钥在服务端的存储与校验(源码级细节)
从源码可以确认密钥的实际用途与校验逻辑:
- 服务端通过
secretsService.get(SecretName.gocardless_secretId)与secretsService.get(SecretName.gocardless_secretKey)读取凭据,并以 JSON 序列化结果作为缓存键维护 GoCardless API 客户端实例(gocardless-service.ts),即密钥一旦更新,客户端实例也会随之重建。 - 服务端暴露
POST /status接口,返回configured: goCardlessService.isConfigured(),用于告知客户端当前是否已配置密钥(app-gocardless.ts)。 - 获取机构列表(
POST /get-banks)前,服务端会先调用setToken():若缓存的 JWT 已过期,则用secret_id/secret_key调用 GoCardless 的/api/v2/token/new/换取新的 access token(gocardless-service.ts)。
因此,"只配置一次"背后的实现是:密钥持久化在服务端,客户端仅负责采集与转发。
三、将 Actual 账户与 GoCardless 银行账户关联
3.1 两种入口
为账户建立 GoCardless 关联有两种方式:
已有账户:点击该账户,进入右上角的...(kebab 菜单),选择Link Account。
新建账户:点击左侧菜单底部的'+ Add account',选择银行同步方式创建新账户。
3.2 关联流程
点击Link your bank account按钮。
从列表中选择你的国家与银行,然后点击Link bank in browser按钮。
点击后浏览器会新开一个标签页,跳转到你的银行,授权 GoCardless 访问你的账户。
在银行页面选择I agree,同意 GoCardless 访问你的支付账户信息。
连接成功后,点击 continue 按钮,允许 GoCardless 完成连接建立。
GoCardless 连接你的银行并拉取账户列表期间,界面会显示进度指示器。
连接建立后,界面会列出可选的银行账户。
最后一步:选中你要同步的账户,点击Link account完成关联。
3.3 关联背后的 Requisition 机制
"关联"在技术实现上对应 GoCardless 的Requisition(授权请求)概念。整个流程由以下源码环节支撑:
- 客户端在用户选择机构后,调用
gocardless-create-web-token,其中将accessValidForDays固定为90 天(gocardless.ts)。 - 服务端
POST /create-web-token会调用goCardlessService.createRequisition()(app-gocardless.ts),其内部依次完成:刷新 token → 查询机构信息 → 判断机构是否支持account_selection/separate_continuous_history_consent等特性 → 携带accessValidForDays、maxHistoricalDays等参数初始化 Requisition(gocardless-service.ts)。若首次请求失败,会自动回退为accessValidForDays = 90、maxHistoricalDays = 89重试。 - 授权完成后,客户端调用
gocardless-poll-web-token轮询状态,成功后弹出账户选择界面,最终以select-linked-accounts模态框确认同步账户(gocardless.ts)。 - 服务端在拉取账户时会校验 Requisition 状态必须为
LN(Linked),否则抛出RequisitionNotLinked异常(gocardless-service.ts)。 - 出于隐私考虑,服务端返回账户列表时会用
sha256String()对 IBAN 做哈希处理后再下发客户端(app-gocardless.ts)。
需要特别注意的是:Requisition 授权是有有效期的。GoCardless 通过 End User Agreement 控制访问时长,当协议过期时,同步会返回ITEM_LOGIN_REQUIRED错误(status: 'expired'),此时需要重新走一遍授权流程(app-gocardless.ts)。
四、同步操作与最佳实践
4.1 Actual 会自动与银行同步吗?
目前不会。Actual 暂不支持银行账户的自动同步,你需要手动触发:进入"All Accounts"页面,点击"Sync"按钮。
手动同步的语义可以从源码中得到印证:同步本质上是让服务端调用 GoCardlessGET /accounts/{id}/transactions/(支持date_from/date_to参数)拉取交易,再经银行适配器归一化后返回给客户端导入(gocardless-api.ts)。
4.2 从零开始使用 GoCardless 的最佳姿势
如果你是第一次用 Actual 搭配 GoCardless,不建议拉取历史数据——这曾给不少用户在后续对账(reconciliation)时带来困扰。更稳妥的流程是:
- 在 Actual 中创建账户,填写最近某一天的准确期初余额(opening account balance);
- 按上文步骤将该账户与 GoCardless 关联;
- 手动同步账户。你会发现只有期初余额日期之后的交易被导入,对账因此变得非常简单。
这一建议与实现机制高度吻合:同步交易时可以携带startDate/endDate参数进行区间拉取,服务端在拉取后还会调用calculateStartingBalance依据当前余额与已导入交易反推期初余额(gocardless-service.ts),保证导入数据的连续性。
4.3 同步频率限额
在 GoCardless 免费档(free tier)下:
- 每月最多连接50 家银行(如果一家银行下挂多个账户,如信用卡+借记卡,仍按1 次连接计);
- 每家银行每天最多同步4 次,该限制为每日限制,月度不设上限;
- 更多细节可查阅 GoCardless 官方 FAQ 中 "Bank Account Data API Usage" 主题(如何计算用量)。
五、深入:银行适配器与交易归一化(进阶)
5.1 BankFactory 与银行专属类
每家银行返回的字段格式并不完全一致(账户命名、交易附言、余额类型各有差异),Actual 通过BankFactory + 银行适配器模式解决这一问题:
BankFactory(account.institution_id)根据机构 ID 返回对应的银行适配器实例(bank-factory.ts);- 仓库内置了大量银行适配器,覆盖 mBank、Revolut、ING、Nationwide、BoursoBank 等机构,每个适配器文件以
机构ID_机构BIC命名,例如revolut_revolt21.ts、mbank_retail_brexplpw.ts(见 banks 目录); - 每个适配器需要实现
IBank接口中的四个方法:normalizeAccount、normalizeTransaction、sortTransactions、calculateStartingBalance。
5.2 normalizeTransaction 的正确写法
normalizeTransaction是最常用的覆写方法,它决定返回给客户端的数据形态。官方推荐的实现模式如下(integration-bank.ts 与 README):
- 需要修改交易字段时,不要改动原始 transaction 对象,先做浅拷贝;
- 函数末尾调用
Fallback.normalizeTransaction(transaction, booked, editedTrans),把修改后的对象作为第 3 个参数传入,由兜底实现统一计算日期、收款人名称(payeeName)与备注(notes)。
示例代码:
import Fallback from './integration-bank'; export default { // ... normalizeTransaction(transaction, booked) { // 创建 transaction 的浅拷贝 const editedTrans = { ...transaction }; // 在拷贝上进行需要的修改 editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured; // 调用兜底方法,传入修改后的对象作为第 3 个参数, // 该方法会基于你的修改计算日期、payeeName 与 notes 字段, // 同时保留原始字段供 UI 映射使用 return Fallback.normalizeTransaction(transaction, booked, editedTrans); } // ... };兜底实现(integration-bank.ts)中值得注意的细节:交易日期按date → bookingDate → bookingDateTime → valueDate → valueDateTime的顺序取值,全部缺失则过滤掉该笔交易(等待银行后续处理完成后再次导入);备注按notes → remittanceInformationUnstructured → remittanceInformationUnstructuredArray依次回退(integration-bank.ts)。
5.3 新增银行适配器的工作流
如果内置适配器无法正确处理某家银行,官方 README(app-gocardless/README.md)给出了完整的新增流程:
- 找到目标银行的机构标识符(institution id,注意旧版参考表已不再维护);
- 启动前端与后端服务;
- 在前端创建关联账户并选择该机构,触发数据抓取,后端会输出该机构原始数据的日志(包括账户属性、前 10 条交易、余额等 JSON 样例),据此填充银行类逻辑;
- 参照
app-gocardless/banks下已有示例新建银行类,文件名与类名遵循既有模式(以机构 ID 命名); - 按日志数据实现
normalizeAccount、normalizeTransaction、sortTransactions、calculateStartingBalance中必要的函数(不需要全部实现); - 将新银行注册到
BankFactory中; - 为新银行补充测试用例(仓库中每个银行适配器都有对应的
.spec.ts测试,例如 revolut_revolt21.spec.ts)。
六、常见问题速查(FAQ)
| 问题 | 答案 |
|---|---|
| Actual 会自动同步银行数据吗? | 不会,需在 "All Accounts" 页面手动点击 "Sync" |
| 免费档每月可连接多少家银行? | 每月最多 50 家,一家银行含多账户仍计 1 次 |
| 每天可同步几次? | 每家银行每天最多 4 次(每日限额,月度不设限) |
| 从零开始怎么用最省心? | 设置近期期初余额 → 关联 → 同步,仅导入期初后的交易 |
| 密钥需要重复输入吗? | 不需要,secret 保存在服务端,仅需配置一次 |
| 授权过期了怎么办? | 重新执行关联流程(同步会提示 ITEM_LOGIN_REQUIRED) |
七、本文要点回顾
- GoCardless 同步需要 Client 与 Server 均 ≥ 23.7.0,且自 2025 年 7 月起 GoCardless 不再接受新的 Bank Account Data 账户注册,存量用户不受影响;
- 密钥在 GoCardless 开发者后台创建后仅可下载一次,配置到 Actual 后持久化保存在服务端;
- 账户关联本质是创建 GoCardless Requisition 并完成银行侧授权,授权受 End User Agreement 有效期约束;
- 同步为手动触发,免费档每月 50 家银行、每日每行 4 次的限额需要留意;
- 若需扩展新银行,可参照
BankFactory + 银行适配器 + normalizeTransaction 兜底模式的既有架构进行集成。
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考