Family Accounting Tool v4.29 API 使用详解
Family Accounting Tool v4.29 外部 API(externalapi)使用详解:appSecret 鉴权机制、全部 12 个端点的请求格式与实测响应、交易搜索语法、批量重构 dry/wet 机制与常见问题。
Family Accounting Tool v4.29 API 使用详解
目标:系统讲解开源家庭记账系统 Family Accounting Tool(facto)v4.29 的外部 API(
/externalapi/*):鉴权机制、全部 12 个端点的请求格式与真实响应、交易搜索语法、批量重构的 dry/wet 机制与常见问题,帮助读者把自动记账、批量改账与数据对接脚本接到自家 facto 上。本文全部请求与响应均在 Ubuntu 22.04.5 LTS(facto v4.29 + MariaDB 10.6,应用监听 9000 端口)上逐条实测通过,响应原文照抄。
一、环境信息
- 应用版本:Family Accounting Tool v4.29(官方发行包
release-v4.29.tar.gz,同 《Ubuntu 22.04 安装 Family Accounting Tool v4.29 指南》 的安装结果;Docker 形态部署见 《Family Accounting Tool v4.29 Docker Compose 部署指南》) - 实测环境:Ubuntu 22.04.5 LTS、OpenJDK 11、MariaDB 10.6.23、应用监听
http://localhost:9000 - 记账建模:发行包自带演示配置
conf/demo-accounting-config.yml——账户 Alice / Bob / Common,储蓄罐 CASH_ALICE(£,GBP 币种)、CARD_ALICE(EUR)等,科目 LIFE / FUN / IN / ACC / ENDOW 等 15 个 - 鉴权密钥(appSecret):
q1w2e3r4-application-secret-2026(本文演示值,取值即配置项play.http.secret.key,见 《Family Accounting Tool v4.29 配置参数详解》 5.1 节) - 源码依据:
app/jvm/src/main/scala/app/controllers/ExternalApi.scala(v4.29 标签,共 602 行)与路由表app/jvm/src/main/resources/routes(查询日期 2026-10-23);文中行为描述均与源码一一对应
二、前置条件
2.1 服务端就绪
外部 API 随 facto 主进程一起监听,无需额外开启开关。先确认服务在跑且健康检查通过:
curl -s http://localhost:9000/externalapi/healthcheck/ -w " [%{http_code}]\n"
实测输出:
OK [200]
若未部署 facto:发行包方式从零安装见 《Ubuntu 22.04 安装 Family Accounting Tool v4.29 指南》;Docker Compose 一键部署见 《Family Accounting Tool v4.29 Docker Compose 部署指南》。
2.2 确定 appSecret
外部 API 每个端点的 URL 末段都是 :appSecret,其正确取值就是配置项 play.http.secret.key(v4.29 源码 ExternalApi.validateApplicationSecret 直接把 URL 中的串与该配置值做全等比对,查询日期 2026-10-23)。确认方法:
- 发行包方式:看
/opt/facto/conf/application.conf中http.secret.key的值; - Docker Compose 方式:看
docker-compose.yml中APPLICATION_SECRET环境变量的值(镜像内配置以${?APPLICATION_SECRET}语法读取该变量覆盖默认值); - 快速验证:拿它调一次任意查询端点,返回 200 即正确(返回 500 见第十一章 Q1)。
⚠️ 注意:play.http.secret.key 同时用于 Web 会话 Cookie 签名,更换后所有 API 调用 URL 里的 secret 必须同步更新,否则全部返回 500(实测见 Q4)。
2.3 客户端工具
只需 curl(任何能发 HTTP 请求的工具均可)。两个关键点:
- 无需登录会话:外部 API 与 Web 界面的登录态完全独立,不需要先
POST /login/,也不需要 Cookie; - POST 请求必须用
text/plain:新增交易端点要求请求体是 YAML 纯文本,源码中require(request.body.asText.isDefined, ...)明确拒绝其他 Content-Type 形态(JSON 表单等都会被拒)。
三、API 总览
facto v4.29 的外部 API 共 12 个端点,全部挂在 /externalapi/ 前缀下(路由表 app/jvm/src/main/resources/routes 原文,查询日期 2026-10-23):
| # | 方法 | 路径模式 | 鉴权 | 作用 |
|---|---|---|---|---|
| 1 | GET | /externalapi/healthcheck/ | 无 | 健康检查 |
| 2 | GET | /externalapi/dbschema/:appSecret/ | appSecret | 返回建库 DDL(全部表结构) |
| 3 | GET | /externalapi/clearoldentitymodifications/:dryOrWet/:appSecret/ | appSecret | 清理旧实体修改日志 |
| 4 | GET | /externalapi/acc/reservoirs/list/:appSecret/ | appSecret | 列出全部支付方式及当前余额 |
| 5 | GET | /externalapi/acc/trans/search/:searchString/:appSecret/ | appSecret | 按搜索串查交易流水 |
| 6 | GET | /externalapi/acc/money/xchgmeasurement/:currency/:appSecret/ | appSecret | 查某外币的全部汇率测量值 |
| 7 | GET | /externalapi/acc/trans/addfromtpl/:tplCode/:appSecret/ | appSecret | 按模板建账(新增一组交易) |
| 8 | POST | /externalapi/acc/trans/add/:appSecret/ | appSecret | 新增一组交易(YAML 请求体) |
| 9 | DELETE | /externalapi/acc/trans/:transactionGroupId/:appSecret/ | appSecret | 按交易组删除整组交易 |
| 10 | GET | /externalapi/acc/money/xchgmeasurement/:date/:currency/:ratio/:appSecret/ | appSecret | 登记一条汇率测量值 |
| 11 | GET | /externalapi/acc/refactor/changecategory/:searchString/:newCategory/:dryOrWet/:appSecret/ | appSecret | 批量改交易科目(支持 dry 预演) |
| 12 | GET | /externalapi/acc/refactor/addtag/:searchString/:tagToAdd/:dryOrWet/:appSecret/ | appSecret | 批量加标签(支持 dry 预演) |
响应形态约定(全文实测归纳):查询端点返回 JSON(pretty-print,无空格紧凑风格);写操作成功统一返回纯文本 OK;失败统一返回 Play 框架默认错误页(HTML)加 HTTP 500,异常详情要看应用日志(见 Q1)。
四、认证与首次调用
4.1 健康检查(无鉴权端点)
healthcheck 是唯一不需要 appSecret 的端点,适合存活探测:
curl -s http://localhost:9000/externalapi/healthcheck/ -w " [%{http_code}]\n"
实测输出:
OK [200]
4.2 appSecret 鉴权实测
带正确 secret 调用查询端点(本例列出支付方式,响应体解读见 5.2 节):
curl -s "http://localhost:9000/externalapi/acc/reservoirs/list/q1w2e3r4-application-secret-2026/" -o /dev/null -w "[%{http_code}]\n"
实测输出:
[200]
换成错误 secret 再调一次:
curl -s "http://localhost:9000/externalapi/acc/reservoirs/list/wrong-secret/" -o /dev/null -w "[%{http_code}]\n"
实测输出:
[500]
500 响应体是 Play 默认错误页,生产模式下不回显异常细节(实测错误页关键行只有一句指路):
<p id="detail">
This exception has been logged with id <strong>8f57i77mp</strong>.
</p>
真实原因在应用日志里(systemd 部署 journalctl -u facto -n 20;Docker Compose 部署 docker compose logs web --tail 20),实测日志关键行:
Caused by: java.lang.IllegalArgumentException: requirement failed: Invalid application secret. Found 'wrong-secret' but should be 'q1w2e3r4-application-secret-2026'
4.3 查询端点的返回骨架
以支付方式列表为例(v4.29 返回 pretty-print JSON,字段含义见 5.2 节),实测响应(演示库初始状态):
[
{
"code":"CASH_COMMON",
"name":"Cash Common",
"ownerAccountCode":"Common",
"hidden":false,
"currencyCode":"EUR",
"balance":0.0,
"lastBalanceCheckDate":null,
"balanceCorrections":[]
},
{
"code":"CARD_COMMON",
"name":"Card Common (£)",
"ownerAccountCode":"Common",
"hidden":false,
"currencyCode":"GBP",
"balance":0.0,
"lastBalanceCheckDate":null,
"balanceCorrections":[]
}
]
(实测输出共 6 个储蓄罐,此处节选前两个;SAVINGS_ALICE 因配置 hidden: true 也在列表中但 hidden 字段为 true——该端点固定返回含隐藏项的全量清单。)
五、流水查询 API
5.1 交易搜索 /acc/trans/search/:searchString/:appSecret/
搜索串与 Web 界面搜索框共用同一套解析器(源码 ComplexQueryFilter,查询日期 2026-10-23),支持「字面量关键词 + 前缀过滤条件」两类写法,可组合:
- 字面量:直接写关键词,按交易描述与标签模糊匹配(不区分大小写);
- 前缀条件:
前缀:值形式,全部合法前缀如下(源码Prefix定义):
| 前缀(含缩写) | 匹配对象 | 示例 |
|---|---|---|
issuer(i、u、user) | 发起人 | issuer:alice |
beneficiary(b) | 受益账户 | beneficiary:Alice |
reservoir(r) | 支付方式(编码或名称) | reservoir:CASH_ALICE |
category(c) | 科目 | category:LIFE |
description | 描述包含 | description:Bakery |
flow(amount、a) | 金额精确匹配(分) | flow:-250 |
minFlow(minAmount、minA、min) | 金额下限(分,含边界) | minFlow:-300 |
maxFlow(maxAmount、maxA、max) | 金额上限(分,含边界) | maxFlow:-1000 |
detail | 详情描述包含 | detail:bread |
tag(t) | 标签包含匹配 | tag:bakery |
tagExact | 标签精确匹配 | tagExact:date-night |
consumedStart(start) | 消费日期起始年 | consumedStart:2026 |
- 组合规则:多个条件用空格分隔即 AND;
-前缀取反(如-category:ACC);短语用引号包裹。URL 中空格写%20,冒号可直接裸放(实测通过)。
实测一(字面量搜索 Bakery,先经 API 新增一笔 £2.50 面包支出,新增过程见 6.1 节):
curl -s "http://localhost:9000/externalapi/acc/trans/search/Bakery/q1w2e3r4-application-secret-2026/"
实测输出:
[
{
"transactionGroupId":5664231616155155528,
"beneficiaryAccountCode":"Alice",
"moneyReservoirCode":"CASH_ALICE",
"categoryCode":"LIFE",
"description":"Bakery",
"flowInCents":-250,
"flowInCentsInReferenceCurrency":-215,
"tags":[],
"createdDate":"2026-10-23",
"transactionDate":"2026-10-23",
"consumedDate":"2026-10-23"
}
]
响应字段逐项说明:
| 字段 | 含义 |
|---|---|
transactionGroupId | 交易组 ID(一组多腿交易的公共编号,删除/引用都以它为单位) |
beneficiaryAccountCode | 受益账户编码 |
moneyReservoirCode | 支付方式编码(空串表示 N/A,不挂资金载体) |
categoryCode | 科目编码 |
description / tags | 描述与标签列表 |
flowInCents | 金额(分),支出为负、收入为正,币种随支付方式 |
flowInCentsInReferenceCurrency | 折算成本位币(EUR)后的分值,换算规则见 7.3 节 |
createdDate / transactionDate / consumedDate | 录入日期 / 交易发生日期 / 计入统计日期(均 yyyy-MM-dd) |
实测二(前缀组合 beneficiary:Alice minFlow:-3,空格编码为 %20):
curl -s "http://localhost:9000/externalapi/acc/trans/search/beneficiary:Alice%20minFlow:-3/q1w2e3r4-application-secret-2026/" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(t['description'], t['flowInCents']) for t in d]"
实测输出:
Bakery -250
解读:当时库里 Alice 名下有两笔——Bakery(−250 分)与 Cinema tickets(−1280 分);minFlow:-3 按分值比较(−250 ≥ −300 满足,−1280 < −300 被排除),所以只返回 Bakery。这验证了 minFlow/maxFlow 的比较单位是「分」而非「元」。
5.2 支付方式与余额 /acc/reservoirs/list/:appSecret/
返回全部支付方式(含隐藏项)及逐罐滚动计算的当前余额,是脚本对账的主入口:
curl -s "http://localhost:9000/externalapi/acc/reservoirs/list/q1w2e3r4-application-secret-2026/" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(r['code'], r['balance'], r['currencyCode']) for r in d]"
实测输出(在 5.1 节两笔交易入库后):
CASH_COMMON 0.0 EUR
CARD_COMMON 0.0 GBP
CARD_BOB 0.0 EUR
CASH_ALICE -2.5 GBP
CARD_ALICE -12.8 EUR
SAVINGS_ALICE 0.0 EUR
解读:CASH_ALICE 余额 −2.5 GBP 正是 Bakery 一笔;CARD_ALICE −12.8 EUR 对应 Cinema tickets。balance 由系统按全部交易与余额核对记录滚动推算(源码 getCurrentBalance),字段 lastBalanceCheckDate 与 balanceCorrections 则把「最近一次数钱核对日期」与「历次账实不符的修正记录」一并返回,便于脚本监控漏账。
5.3 汇率测量值查询 /acc/money/xchgmeasurement/:currency/:appSecret/
curl -s "http://localhost:9000/externalapi/acc/money/xchgmeasurement/GBP/q1w2e3r4-application-secret-2026/"
实测输出:
[["2026-10-01",0.86],["2026-10-23",0.86],["2026-10-23",0.86]]
返回格式是 [日期, 参考币兑该外币汇率] 的二维数组,按日期升序。解读:本文实测前演示库已有两条 2026-10-23 的历史测量值(读者在全新库上首次查询应为 []),加上 7.1 节登记的 2026-10-01 一条,共 3 条。该端点只读不写;写入见 7.1 节。
六、流水写入 API
6.1 新增交易 POST /acc/trans/add/:appSecret/
请求体是 YAML 纯文本,顶层键固定为 transactions,列表中每个元素是一笔交易腿。字段定义(源码 TransactionParsableValue.supportedKeyValuePairs):
| 字段 | 必填 | 说明 |
|---|---|---|
beneficiaryCode | 是 | 受益账户编码,必须是配置 accounts 中已有编码 |
moneyReservoirCode | 是 | 支付方式编码(含隐藏罐);空串表示 N/A |
categoryCode | 是 | 科目编码,必须是配置 categories 中已有编码 |
description | 是 | 交易描述 |
flowAsFloat | 是 | 金额(元,可带小数);系统乘 100 取整为分,支出为负 |
detailDescription | 否 | 详情描述,默认空串 |
tags | 否 | 标签列表,默认空;标签只能含 [a-zA-Z0-9-_@$&()+=!.<>#;: ] 字符(源码 Tags.validTagRegex) |
transactionDate | 否 | 交易发生日期,格式 yyyy-MM-dd,缺省为今天 |
consumedDate | 否 | 计入统计日期,格式 yyyy-MM-dd,缺省为今天(源码 map.optional("consumedDate", defaultValue = today)) |
实测一(最小字段集:Alice 用现金 £2.50 买面包,挂 LIFE 科目):
curl -s -X POST "http://localhost:9000/externalapi/acc/trans/add/q1w2e3r4-application-secret-2026/" -H "Content-Type: text/plain" --data-binary $'transactions:\n - beneficiaryCode: Alice\n moneyReservoirCode: CASH_ALICE\n categoryCode: LIFE\n description: Bakery\n flowAsFloat: -2.5\n detailDescription: Bread and milk\n' -w " [%{http_code}]\n"
实测输出:
OK [200]
实测二(带标签与指定交易日期):
curl -s -X POST "http://localhost:9000/externalapi/acc/trans/add/q1w2e3r4-application-secret-2026/" -H "Content-Type: text/plain" --data-binary $'transactions:\n - beneficiaryCode: Alice\n moneyReservoirCode: CARD_ALICE\n categoryCode: FUN\n description: Cinema tickets\n flowAsFloat: -12.8\n tags:\n - date-night\n transactionDate: 2026-10-20\n' -w " [%{http_code}]\n"
实测输出:
OK [200]
验证入库结果(transactionDate 为指定的 2026-10-20,consumedDate 缺省落为今天 2026-10-23):
curl -s "http://localhost:9000/externalapi/acc/trans/search/Cinema/q1w2e3r4-application-secret-2026/"
实测输出:
[
{
"transactionGroupId":3003257207310387907,
"beneficiaryAccountCode":"Alice",
"moneyReservoirCode":"CARD_ALICE",
"categoryCode":"FUN",
"description":"Cinema tickets",
"flowInCents":-1280,
"flowInCentsInReferenceCurrency":-1280,
"tags":[
"date-night"
],
"createdDate":"2026-10-23",
"transactionDate":"2026-10-20",
"consumedDate":"2026-10-23"
}
]
字段校验失败实测(科目编码写错):
curl -s -X POST "http://localhost:9000/externalapi/acc/trans/add/q1w2e3r4-application-secret-2026/" -H "Content-Type: text/plain" --data-binary $'transactions:\n - beneficiaryCode: Alice\n moneyReservoirCode: CASH_ALICE\n categoryCode: NONEXIST\n description: Bad test\n flowAsFloat: -1\n' -o /dev/null -w "[%{http_code}]\n"
实测输出:
[500]
真实错误在应用日志(校验信息逐字段列出合法值,实测日志关键行):
- [transactions > Bad test] categoryCode=NONEXIST was not recognized (valid values: Set(LIFE, FUN, RECUR, ONE, PRES, CLOTH, MED, TRANS, FINAN, IN, TAX, ACC, ENDOW, BIG, INVEST)
⚠️ 注意:一笔 POST 可以带多条交易腿(transactions: 列表多元素),同一请求内的所有腿共用一个 transactionGroupId,删除时按组整体删除。
6.2 模板建账 GET /acc/trans/addfromtpl/:tplCode/:appSecret/
按配置文件 templates 节中声明的模板编码一键建账(模板定义见 《Family Accounting Tool v4.29 配置参数详解》 5.9 节)。实测模板 wages-alice(工资 910 EUR 入账,模板编码为固定值、不含占位符):
curl -s "http://localhost:9000/externalapi/acc/trans/addfromtpl/wages-alice/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
实测输出:
OK [200]
curl -s "http://localhost:9000/externalapi/acc/trans/search/Wages/q1w2e3r4-application-secret-2026/"
实测输出:
[
{
"transactionGroupId":436393162164211230,
"beneficiaryAccountCode":"Alice",
"moneyReservoirCode":"CARD_ALICE",
"categoryCode":"IN",
"description":"Wages",
"flowInCents":91000,
"flowInCentsInReferenceCurrency":91000,
"tags":[],
"createdDate":"2026-10-23",
"transactionDate":"2026-10-23",
"consumedDate":"2026-10-23"
}
]
⚠️ 注意含 ${account.*} 占位符的模板在本端点的真实行为(实测):外部 API 调用模板时不指定账户,占位符一律按「空账户实例」解析(源码 ExternalApi.toTransactions 传入 Account.nullInstance,其编码为 NULL_INSTANCE)——${account.code} 展开为 NULL_INSTANCE、${account.defaultCashReservoir.code} 展开为空串(即 N/A 储蓄罐)。实测模板 bakery(描述 Bakery、£2.10、受益 Common,但其支付方式写成 ${account.defaultCashReservoir.code}):
curl -s "http://localhost:9000/externalapi/acc/trans/addfromtpl/bakery/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
实测输出(不报错但资金载体落为 N/A):
OK [200]
curl -s "http://localhost:9000/externalapi/acc/trans/search/Bakery/q1w2e3r4-application-secret-2026/" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(t['transactionGroupId'], t['beneficiaryAccountCode'], repr(t['moneyReservoirCode']), t['flowInCents']) for t in d]"
实测输出:
7525246305348713784 'Common' '' -210
5664231616155155528 'Alice' 'CASH_ALICE' -250
第一行即模板建账产物:moneyReservoirCode 为空串(N/A),不挂任何储蓄罐。结论:经外部 API 调模板建账,只对编码与受益人都写死(无 ${account.*} 占位符)的模板才能得到预期储蓄罐;含占位符的模板请改用 6.1 节的 POST /acc/trans/add/ 自行指定字段。
6.3 删除交易 DELETE /externalapi/acc/trans/:transactionGroupId/:appSecret/
按交易组整组删除(组内所有腿一起删)。实测删除 6.2 节两笔测试产物:
curl -s -X DELETE "http://localhost:9000/externalapi/acc/trans/436393162164211230/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
curl -s -X DELETE "http://localhost:9000/externalapi/acc/trans/7525246305348713784/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
实测输出:
OK [200]
OK [200]
验证删除结果(Wages 已查不到,返回空数组):
curl -s "http://localhost:9000/externalapi/acc/trans/search/Wages/q1w2e3r4-application-secret-2026/"
实测输出:
[]
七、汇率登记 API
7.1 登记测量值 GET /acc/money/xchgmeasurement/:date/:currency/:ratio/:appSecret/
三个业务参数直接进 URL:日期(严格 yyyy-MM-dd,源码 DateStringConversions.stringToDateStrict 解析失败即 500)、外币 ISO 代码(必须是配置中声明为外币的币种,如 GBP;本位币 EUR 不接受)、比率(参考币兑外币,浮点数)。实测登记 2026-10-01 的 GBP 汇率 0.86:
curl -s "http://localhost:9000/externalapi/acc/money/xchgmeasurement/2026-10-01/GBP/0.86/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
实测输出:
OK [200]
7.2 查询与核对
curl -s "http://localhost:9000/externalapi/acc/money/xchgmeasurement/GBP/q1w2e3r4-application-secret-2026/"
实测输出:
[["2026-10-01",0.86],["2026-10-23",0.86],["2026-10-23",0.86]]
(首条即 7.1 节登记值;后两条为本文实测前库中已存在的历史数据。)同日可登记多条,查询端按日期升序返回。
7.3 汇率如何影响金额折算
交易响应里的 flowInCentsInReferenceCurrency 按「该支付方式币种 → 本位币」换算:flowInCentsInReferenceCurrency = flowInCents × 参考币兑外币汇率(取交易日期之前最近一条测量值)。两个实测锚点:
- 5.1 节 Bakery(CASH_ALICE,GBP 罐,−250 分):当时库中已有 2026-10-23 的 0.86 测量值,折算 −250 × 0.86 = −215,与实测输出一致;
- 无测量值时回退比率 1.0(源码
CurrencyValueManager.ratioReferenceToForeignCurrency的None => 1.0分支),所以 6.2 节 wages-alice(EUR 罐)的flowInCentsInReferenceCurrency与flowInCents相同。
本位币固定为 EUR:源码 Currency.default = Eur(旁注 TODO: Make this configurable,v4.29 尚不可配置)。EUR/GBP/USD 为内置标准币种,其余币种走 General 通用编码。
八、批量重构 API
8.1 dry/wet 双阶段机制
两个重构端点都要求在 URL 里显式声明 dryOrWet:
dry:只计算并返回「哪些交易会被改」的报告,不写库;wet:真正落库。
规范用法是先 dry 核对报告、再 wet 执行。报告为多行纯文本,字段含义见 8.2 节解读。
8.2 批量改科目 /acc/refactor/changecategory/:searchString/:newCategory/:dryOrWet/:appSecret/
把搜索串命中的交易科目改成 newCategory(必须是配置中已有科目)。实测把 5.1 节的 Bakery 从 LIFE 改为 FUN,先 dry:
curl -s "http://localhost:9000/externalapi/acc/refactor/changecategory/Bakery/FUN/dry/q1w2e3r4-application-secret-2026/" -w "[%{http_code}]\n"
实测输出:
searchString = Bakery
update = Changing category to FUN
#transactions that match searchString = 1
#transactions that will be edited = 1
#entity modifications = 2
Run = dry
Transactions that will be edited:
- Bakery (Category(LIFE), List())
[200]
报告逐行解读:#transactions that match searchString 为命中数;#transactions that will be edited 为剔除「改了等于没改」之后的实际改动数;#entity modifications 是落库的实体变更条数(每笔交易的重构实现为「删旧 + 增新」两条修改,故 1 笔对应 2 条);末尾列出每笔将改交易的描述、当前科目与标签。确认无误后 wet 执行:
curl -s "http://localhost:9000/externalapi/acc/refactor/changecategory/Bakery/FUN/wet/q1w2e3r4-application-secret-2026/" | head -5
实测输出:
searchString = Bakery
update = Changing category to FUN
#transactions that match searchString = 1
#transactions that will be edited = 1
#entity modifications = 2
验证结果(科目已从 LIFE 变 FUN):
curl -s "http://localhost:9000/externalapi/acc/trans/search/Bakery/q1w2e3r4-application-secret-2026/" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(t['description'], t['categoryCode']) for t in d]"
实测输出:
Bakery FUN
8.3 批量加标签 /acc/refactor/addtag/:searchString/:tagToAdd/:dryOrWet/:appSecret/
给命中交易追加标签(已含该标签的交易自动跳过;标签须满足 6.1 节的合法字符规则,否则请求直接 500)。实测给 Bakery 追加标签 bakery-test,先 dry:
curl -s "http://localhost:9000/externalapi/acc/refactor/addtag/Bakery/bakery-test/dry/q1w2e3r4-application-secret-2026/" | tail -4
实测输出(报告尾部,结构与 8.2 节相同,update = Adding tag 'bakery-test'):
Run = dry
Transactions that will be edited:
- Bakery (Category(FUN), List())
wet 执行并验证:
curl -s "http://localhost:9000/externalapi/acc/refactor/addtag/Bakery/bakery-test/wet/q1w2e3r4-application-secret-2026/" > /dev/null
curl -s "http://localhost:9000/externalapi/acc/trans/search/Bakery/q1w2e3r4-application-secret-2026/" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(t['description'], t['tags']) for t in d]"
实测输出:
Bakery ['bakery-test']
九、辅助端点
9.1 数据库结构 /dbschema/:appSecret/
返回全部 6 张表的建表 DDL(纯文本,实测全文 18 行),用于备份前核对结构或外部工具对接字段。实测输出节选(首两表):
OK
## USERS ##
create table `USERS` (`loginName` TEXT NOT NULL,`passwordHash` TEXT NOT NULL,`name` TEXT NOT NULL,`isAdmin` BOOLEAN NOT NULL,`expandCashFlowTablesByDefault` BOOLEAN NOT NULL,`expandLiquidationTablesByDefault` BOOLEAN NOT NULL,`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,`lastUpdateTime` BLOB NOT NULL)
## TRANSACTIONS ##
create table `TRANSACTIONS` (`transactionGroupId` BIGINT NOT NULL,`issuerId` BIGINT NOT NULL,`beneficiaryAccountCode` TEXT NOT NULL,`moneyReservoirCode` TEXT NOT NULL,`categoryCode` TEXT NOT NULL,`description` TEXT NOT NULL,`flow` BIGINT NOT NULL,`detailDescription` TEXT NOT NULL,`tagsString` TEXT NOT NULL,`data` TEXT NOT NULL,`createdDate` TIMESTAMP NOT NULL,`transactionDate` TIMESTAMP NOT NULL,`consumedDate` TIMESTAMP NOT NULL,`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY)
(其余 4 张表为 TRANSACTION_GROUPS、BALANCE_CHECKS、EXCHANGE_RATE_MEASUREMENT、ENTITY_MODIFICATION_ENTITY,与发行包安装时 SHOW TABLES 的建表结果一致。)
9.2 清理旧实体修改记录 /clearoldentitymodifications/:dryOrWet/:appSecret/
facto 把每次实体增删改都记入 ENTITY_MODIFICATION_ENTITY 表(多端同步用),长期运行会持续膨胀;该端点清理已无同步价值的历史记录,同样支持 dry 预演。实测 dry:
curl -s "http://localhost:9000/externalapi/clearoldentitymodifications/dry/q1w2e3r4-application-secret-2026/" -w " [%{http_code}]\n"
实测输出:
OK
Total number of entity modifications: 55
Number of entity modifications to be removed: 0
[200]
解读:演示库共 55 条修改记录,其中 0 条满足清理条件(近期记录保留以支撑客户端同步);将 dry 换成 wet 即执行删除。家庭单库场景建议低频(如每季度)执行一次。
十、过滤、排序与分页约定
- 无分页:搜索端点一次返回全部命中交易(数组),无 page/limit 参数;数据量极大的家庭应配合
minFlow/consumedStart等前缀条件缩小命中面。 - 排序固定:搜索结果按
consumedDate确定性倒序返回(最新计入日期在前;源码AppDbQuerySorting.Transaction.deterministicallyByConsumedDate.reversed),重构端点按createdDate确定性倒序选目标,均不可通过参数改变。 - 过滤语法与界面一致:搜索串与 Web 界面图表/搜索框共用
ComplexQueryFilter解析器,界面上调通的过滤条件在 API 中原样可用。 - URL 编码:空格必须编码为
%20;冒号、连字符可裸放(实测beneficiary:Alice%20minFlow:-3直接 200);中文与特殊字符建议整体--data-urlencode或预先 URL 编码。
十一、常见问题
Q1:调用返回 HTTP 500,错误页里只有 This exception has been logged with id ...,怎么定位?
生产模式错误页不回显异常详情(4.2 节实测),真实原因在应用日志:systemd 部署用 journalctl -u facto -n 20,Docker Compose 部署用 docker compose logs web --tail 20。本文实测的两类高频错误日志行:密钥错 IllegalArgumentException: requirement failed: Invalid application secret. Found '...' but should be '...';编码错 categoryCode=NONEXIST was not recognized (valid values: Set(...))。
Q2:为什么有的模板经 addfromtpl 建账后没有挂到预期的储蓄罐?
见 6.2 节实测:外部 API 调模板时占位符按空账户实例(NULL_INSTANCE)解析,${account.defaultCashReservoir.code} 落为空串(N/A 罐)。含 ${account.*} 占位符的模板请改用 POST /acc/trans/add/ 逐字段指定;只有模板编码、受益人、支付方式全部写死的模板(如 wages-alice)才能经 addfromtpl 得到完整预期结果。
Q3:flowInCents 与 flowInCentsInReferenceCurrency 为什么不一样?
前者是支付方式币种下的原始分值,后者按登记的汇率折算成本位币 EUR(7.3 节实测:GBP 罐 −250 分 × 0.86 = −215 分)。库中没有该外币测量值时按比率 1.0 回退;需要正确折算请先用 7.1 节端点登记汇率。
Q4:更换 play.http.secret.key 后所有 API 调用都 500 了?
appSecret 与该配置项是同一个值,更换密钥后所有调用 URL 中的 secret 必须同步替换(2.2 节);旧 secret 调用的实测报错见 4.2 节与 Q1。
Q5:minFlow/maxFlow 的单位是元还是分?
分(cents)。5.1 节实测 minFlow:-3 放行了 −250 分的 Bakery、排除了 −1280 分的 Cinema tickets——比较发生在分值层面,写条件时按分计算。
十二、参考链接
- 外部 API 全部实现(v4.29):
https://github.com/nymanjens/facto/blob/v4.29/app/jvm/src/main/scala/app/controllers/ExternalApi.scala - 路由表(v4.29):
https://github.com/nymanjens/facto/blob/v4.29/app/jvm/src/main/resources/routes - 搜索语法解析器:
https://github.com/nymanjens/facto/blob/v4.29/app/shared/src/main/scala/app/common/accounting/ComplexQueryFilter.scala - 演示记账配置(账户/储蓄罐/模板编码出处):
https://github.com/nymanjens/facto/blob/v4.29/conf/demo-accounting-config.yml - 本专栏相关文档:《Family Accounting Tool v4.29 介绍与选型指南》(API 在整体架构中的定位)、《Ubuntu 22.04 安装 Family Accounting Tool v4.29 指南》、《Family Accounting Tool v4.29 Docker Compose 部署指南》、《Family Accounting Tool v4.29 配置参数详解》