LedgerSMB 1.13.8 API 使用详解
LedgerSMB 1.13.8 内置 HTTP API 实战:company_path 挂载规则、login.pl 认证与会话 Cookie、session 会话端点、集合资源与 8 种导出格式、POST/PUT/ETag 乐观锁、PATCH/DELETE 与 501 能力边界,请求响应全部 2026-10-24 实测回显。
LedgerSMB 1.13.8 API 使用详解
目标:面向需要把 LedgerSMB 接入自研系统、做批量数据维护或构建集成脚本的读者,讲清 1.13.8 内置 HTTP API 的挂载方式、认证会话机制、可用的资源端点、乐观锁更新流程,以及实测探明的功能边界(哪些能力可用、哪些尚未开放)。本文所有请求与响应均于 2026-10-24 在本专栏部署篇所建的 1.13.8 实例(演示账套 demo1,示例账号 admin)上实测取得,回显逐行照抄,未做任何推测性补写。
一、API 形态总览
LedgerSMB 1.13 系列在传统 login.pl / erp.pl Web 表单路由之外,内置了一层 REST 风格的 JSON API。它不是独立服务,而是同一应用内的一组路由:以 PSGI 方式托管时,API 与 Web UI 共用同一个 HTTP 端口(本实验环境为 8130),由应用内的路由器分发(发行包内 lib/LedgerSMB/Router.pm)。
挂载前缀公式(1.13.8 实测):
http://<host>:<port>/<company_path>/erp/api/v0/
其中 company_path 是账套名的 MD5 十六进制值。实测验证:账套名 demo1 的 MD5 为 e368b9938746fa090d6afd3628355133(echo -n demo1 | md5sum 结果),以此拼出的 API 前缀全部命中。这一设计在发行包内 lib/LedgerSMB/PSGI.pm 中实现(公司路径取公司名的 MD5),目的是让 URL 不直接暴露真实数据库名。
能力边界速览(全部为 2026-10-24 实测结论,细节见后文):
| 能力 | 实测结论 |
|---|---|
| 认证建立会话 | ✅ POST /login.pl?action=authenticate,JSON 凭据 |
| 会话信息查询 | ✅ GET .../session(偏好、角色、密码有效期) |
| 基础数据读列表 | ✅ 商品组/币别国家/语言/科目/模板/菜单节点等 |
| 单条读取 | ✅ 带 ETag 响应头 |
| 新建资源 | ✅ POST 返回 201 Created + ETag |
| 全量更新 | ✅ PUT + If-Match 乐观锁(强制) |
| 部分更新 | ⚠️ PATCH 实测 500(路由层异常,见第六节) |
| 删除资源 | ⚠️ DELETE 带 If-Match 实测 403 Forbidden |
| 发票/订单域 | ❌ GET /invoices、GET /orders 返回 501 Not Implemented |
| 接口自描述 | ❌ /erp/api/v0/doc 等文档端点实测 404,无 OpenAPI/Swagger 输出 |
💡 读到 501 或 403 不必怀疑自己拼错了 URL——它们就是 1.13.8 的"真实边界"。LedgerSMB 的 API 覆盖是渐进式的:基础资料与商品类资源已经 REST 化,而发票、订单等核心单据域尚未开放,仍需走 Web 表单或官方 CLI 途径。
二、认证与会话:login.pl 三步走
API 没有独立的 token 发放端点,会话载体是 HttpOnly Cookie。完整认证分两步,外加一个可选的会话确认请求。
第 1 步:获取匿名会话 Cookie。对登录页发一个普通 GET,服务器会种下一个匿名会话 Cookie:
curl -s -c jar.txt -D - http://localhost:8130/login.pl
实测响应头(逐行照抄):
HTTP/1.1 200 OK
Set-Cookie: LedgerSMB=01nzLPYaSo5CSZ3UfZpy_PRSIEvMj6A0EE-_wErDS2w~1800590805~WymMuFl7NFz9Ug5oG476a05AeWBiYhKZJQMiWBuZUkgsJq-EFUebz98SkfyH4b-i~aAJRGMBOfoRMGcD8ODaeIX7Qx1NUC_0xJ9HGW3GU6rE~2; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT; SameSite=Strict; HttpOnly
第 2 步:提交认证。以 JSON 体内联提交登录名、密码与账套名:
curl -s -b jar.txt -c jar.txt -D - -H 'Content-Type: application/json' \
-X POST 'http://localhost:8130/login.pl?action=authenticate' \
-d '{"login":"admin","password":"q1w2e3r4","company":"demo1"}'
实测响应(逐行照抄,Cookie 值因会话而异):
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: LedgerSMB=nZecFe-8Fh5UUqr6KAF96KHYa6uyPVZtXXEp2aGb0_0~1800590806~hDL0CUao6fSYmLZDuDdr4Y4ENwSeokvwR-oQUgia8puBxyOBr4JbI9bGTmHqUODLUi9ar9RHiM2Rnz8Wi7rkmNNIBL-Wgws4yamOucnU9IRWBpAcc8qt0gNCWGL6tvggKN7cDtPzjaRCPzqUvhOiPX_Zk1IoitHJzyibAlH9ZTKirzeU1J0yo0d3u7yW7F7qU1EWkwWFu3hSv5dEVgYJA8SZSLYoOjoqEGcuqt9anyOs01fqGKlpXKfky4ZnUL0PSv1_GYickUsgG-BYCQZCZJ4KrUj4VW8fOS2h1qm_jB9i91AfLpKghJLp6QC2KViy-7-eeDGpwITGwb41zzZHHhExeJJzrm2VOs26RqYaDvFvuXdLf0bwdZ8Howb2FiAo~B_qUJ-xZ-c-x5IYeySaua_WHVhUP4QjBUixh6QiV2TY~2; path=/e368b9938746fa090d6afd3628355133/; SameSite=Strict; HttpOnly
{ "target": "e368b9938746fa090d6afd3628355133/erp.pl?user=admin" }
三个关键信息都在这一步给出:
- 响应体
target字段直接返回了该账套的 API 挂载前缀(demo1的 MD5),客户端无须自己计算 MD5; - 认证成功的会话 Cookie 的
path被收窄到/<company_path>/——会话凭证按账套隔离,同浏览器登录多账套互不干扰(这与 1.13 主打的多公司并发登录特性吻合); - Cookie 属性带
HttpOnly与SameSite=Strict,浏览器侧脚本无法读取,跨站请求也不会携带。
__action 等价写法:POST /login.pl?__action=authenticate(双下划线)实测返回完全相同的 200 与响应体,与部署篇的登录验收命令一致。两种写法在 1.13.8 中等价。
认证失败矩阵(全部实测,响应体同为单行文案):
| 场景 | 实测状态码 | 响应体 |
|---|---|---|
| 密码错误 | 401 Unauthorized | Credentials invalid or session expired |
| 账套不存在(company=nosuchdb) | 401 Unauthorized | Credentials invalid or session expired |
| 不带 Cookie 直接访问 API | 401 Unauthorized | Credentials invalid or session expired |
| 伪造 company_path(全 0 的 MD5) | 401 Unauthorized | Credentials invalid or session expired |
💡 四种失败场景返回同一段文案,服务端不区分"密码错"与"账套不存在"——这是刻意设计,避免向探测者泄露数据库是否存在。做集成时不要试图从 401 文案里区分失败原因,应引导用户检查账号、密码、账套名三要素。
三、会话信息端点:GET session
带着认证 Cookie 请求 API 根下的 session 端点,可确认会话有效并读取当前用户的偏好与权限:
curl -s -b jar.txt \
http://localhost:8130/e368b9938746fa090d6afd3628355133/erp/api/v0/session
实测响应(逐行照抄,roles 数组较长,此处截取首尾):
{
"preferences" : {
"dateformat" : "yyyy-mm-dd",
"numberformat" : "1000.00",
"printer" : null,
"language" : null,
"stylesheet" : null
},
"roles" : [
"account_all",
"account_create",
"account_delete",
"account_edit",
"account_link_description_create",
"ap_all",
...(中间省略 120 余项)...
"users_manage",
"voucher_delete",
"warehouse_create",
"warehouse_edit",
"yearend_reopen",
"yearend_run"
],
"password_expiration" : "PT15H32M17.370174S"
}
三个字段的含义:
preferences:用户偏好(日期格式yyyy-mm-dd、数字格式1000.00),与登录后 Web 界面使用的一致;做本地化渲染时可直接取用。roles:当前用户在本账套内的权限角色全量清单(实测 admin 账号共 136 条,覆盖科目、应收应付ap_*/ar_*、库存inventory_*、订单orders_*、系统管理system_admin等)。API 的权限模型与 Web UI 完全同源——同一用户调 API 能做什么,与其在界面上的角色一致。password_expiration:ISO 8601 时长格式(PT15H32M17.370174S即约 15 小时 32 分),当前密码距过期的剩余时间,可用于客户端提前提醒改密。
四、集合资源与导出链接
API 的集合端点返回统一的 JSON 骨架:_links(导出链接)+ items(数据项)。以商品组为例:
curl -s -b jar.txt \
http://localhost:8130/e368b9938746fa090d6afd3628355133/erp/api/v0/products/partsgroups
实测响应骨架(逐行照抄,items 截取):
{
"_links" : [
{
"rel" : "download",
"title" : "PDF",
"href" : "?format=PDF"
},
{
"rel" : "download",
"title" : "PS",
"href" : "?format=PS"
},
{
"href" : "?format=XLS",
"title" : "XLS",
"rel" : "download"
},
...(XLSX、ODS、CSV、TXT、HTML 同构)...
],
"items" : [
{
"id": 1,
"_meta": { "ETag": "e0721b361b77f26fa04937b033bd3d96" },
"parent": null,
"description": "API 实测商品组-已更新"
},
...
]
}
两个值得记住的协议细节:
_links导出矩阵:每个列表端点都自带 8 种导出格式链接(实测 titles:PDF、PS、XLS、XLSX、ODS、CSV、TXT、HTML),在列表 URL 后拼?format=CSV即可直接拿到对应格式文件。给非技术同事导数据时,这一招比写脚本更快。items[]._meta.ETag:列表内每条记录都内嵌了自己的 ETag。做批量更新时不必逐条 GET 单取 ETag——列表一次拿全,直接作为If-Match使用。
实测可用的集合端点(2026-10-24 全部验证为 200):
| 端点 | 实测规模/结构 | 说明 |
|---|---|---|
/products/partsgroups | 3 项(实验账套) | 商品组,{id, description, parent} |
/products/pricegroups | 200 | 价格组 |
/products/warehouses | 200 | 仓库 |
/products/goods/ | 200(空数组 []) | 商品/服务目录(注意尾斜杠,见下) |
/countries | 263 项 | 国家/币别基础资料,含 code、localizedName |
/languages | 43 项 | 语言资料,如 {code: "ar_EG", description: "Arabic (Egypt)"} |
/menu-nodes | 顶层数组 | 菜单树,{id, label, parent, menu, url} 结构 |
/accounts/ | 科目数组 | 科目表,含 accno、label、category(如 "1061--支票户头",category: "A") |
/templates/ | 55 条 | 单据模板清单,每条含 template_name、format、src(内容下载路径 /erp/api/v0/templates/<id>/content) |
尾斜杠语义:集合路由是否带尾斜杠不可随意省略。实测同一前缀下 GET .../erp/api/v0/goods(无斜杠)返回 404,而 GET .../erp/api/v0/goods/(有斜杠)返回 200 与空数组。写集成代码时把 URL 常量与官方路由完全对齐,避免这类"看不见的 404"。
五、资源创建与乐观锁更新:POST / PUT / ETag
写操作是这套 API 设计最完整的部分,核心机制是 ETag 乐观锁:每次成功修改后资源的 ETag 都会变化,更新必须携带最新 ETag 作为 If-Match 前置条件,否则被拒。以下序列全部为 2026-10-24 在 demo1 账套上的实测回显(逐行照抄)。
创建:POST 提交 JSON,返回 201 Created、ETag 响应头与新建资源体:
curl -s -b jar.txt -D - -H 'Content-Type: application/json' \
-X POST .../erp/api/v0/products/partsgroups \
-d '{"description":"D5-API-evidence-group"}'
实测响应:
HTTP/1.1 201 Created
ETag: "f617be7f04c8aef0b5ea484b6754b9c4"
{
"id" : 4,
"description" : "D5-API-evidence-group",
"parent" : null
}
单条读取:GET .../products/partsgroups/4 返回 200,响应头携带同一 ETag(ETag: "f617be7f04c8aef0b5ea484b6754b9c4"),响应体为 {description, parent, id} 三键。
更新三态(1.13.8 实测矩阵,这是本文最重要的实测结论之一):
| 请求形态 | 实测结果 | 语义 |
|---|---|---|
PUT 不带 If-Match | 500 Internal Server Error,响应体 Internal Server Error | 服务端异常而非规范化的 428——1.13.8 对缺失前置条件没有给出友好错误 |
PUT 带过期/错误 If-Match(如 "deadbeefdeadbeefdeadbeefdeadbeef") | 409 Conflict(空响应体) | 并发冲突拒绝:你手里的版本已经不是最新 |
PUT 带当前 If-Match | 200 OK + 新 ETag | 更新成功 |
带当前 ETag 的成功更新实测:
curl -s -b jar.txt -D - -H 'Content-Type: application/json' \
-H 'If-Match: "f617be7f04c8aef0b5ea484b6754b9c4"' \
-X PUT .../erp/api/v0/products/partsgroups/4 \
-d '{"id":4,"description":"put-with-current-etag-ok"}'
实测响应:
HTTP/1.1 200 OK
ETag: "6a13e70925744985f7451f46c3a97eb5"
{
"parent" : null,
"description" : "put-with-current-etag-ok",
"id" : 4
}
注意两点:ETag 已变为新值 6a13e70925744985f7451f46c3a97eb5,下一次更新必须用它;PUT 请求体必须包含 id 字段且与路径一致。
请求体校验:PUT 缺少 id 字段时实测返回 400 Bad Request,错误结构逐行照抄:
{
"error" : 1,
"errors" : [
{
"context" : [
{
"instance_path" : "/",
"schema_path" : "/allOf/1/required/0",
"message" : "instance does not have required property \"id\""
}
],
"message" : "request body error"
}
],
"msg" : "LedgerSMB API errors"
}
这是标准的 JSON Schema 校验回显(schema_path 直接指向校验规则位置)。集成端可以直接解析 errors[].context[].message 给出人话提示。
💡 乐观锁的意义在于多人记账场景:财务与库管可能同时改同一条基础资料。HTTP 标准中
If-Match失败通常是 412 Precondition Failed,而 LedgerSMB 1.13.8 实测返回 409 Conflict(空体)——集成代码应同时兼容 409/412 两个状态码,并遵循"重新 GET 拿新 ETag 再重试"的标准恢复流程。
六、PATCH 与 DELETE 的现实边界
这一节是给计划做深度集成者最实用的"避坑"实测。1.13.8 的 PATCH 与 DELETE 在商品类资源上路由存在,但执行层未达可用状态。
PATCH:无论 JSON 体形态还是查询参数形态,实测均返回 500:
PATCH .../products/partsgroups/4 (If-Match + {"description":"patch-json-body"})
→ HTTP/1.1 500 Internal Server Error / Internal Server Error
PATCH .../products/partsgroups/4?description=patch-query-param (无请求体)
→ HTTP/1.1 500 Internal Server Error / Internal Server Error
服务端日志捕获到根因(逐行照抄):
Can't call method "message" on an undefined value at /srv/ledgersmb/lib/LedgerSMB/Router.pm line 501, <$io> line 1.
2026/10/24 04:08:48 Unexpected exit; rolling back current db transaction
即路由框架层在处理 PATCH 请求时崩溃(Router.pm 第 501 行对未定义值调用了 message 方法),事务被回滚——这不是业务规则拒绝,而是 1.13.8 的代码缺陷。结论:部分更新不要走 PATCH,改用 PUT 全量提交。
DELETE:不带 If-Match 实测 500;带上 If-Match 实测 403 Forbidden(空响应体),服务端日志显示事务正常提交后返回 403(Server response: 403 + Committing current db transaction)——即删除请求被设计性拒绝,资源实际不会被删除(本文实测创建的商品组 id=4 也因此仍留存于演示账套中)。
给集成者的替代路径:确需删除/停用基础资料时,走 Web UI 的对应管理页面(部分资源支持"废弃/obsolete"标记而非物理删除),或在数据库层按官方备份流程操作后谨慎处理。把 DELETE 403 视为"1.13.8 尚未开放删除能力"的信号,不要在重试逻辑里死循环。
七、尚未开放的资源域:501
核心单据域尚未 REST 化。实测:
GET .../erp/api/v0/invoices → HTTP/1.1 501 Not Implemented(空响应体)
GET .../erp/api/v0/orders → HTTP/1.1 501 Not Implemented(空响应体)
501 的语义是"服务器已理解请求路径,但该能力未实现"——与 404(路由不存在)和 403(拒绝执行)都有区别。这也解释了 LedgerSMB API 的当前定位:适合基础资料同步与查询类集成,开票/订单等业务单据仍应以 Web 界面或账套内直接操作为主。选型评估(见本专栏介绍篇)中把 LedgerSMB 定位为"账务正确性优先",API 的渐进式覆盖与之同频。
八、错误响应速查表
全部状态码均来自 2026-10-24 实测,可直接作为集成测试的断言基线:
| 状态码 | 触发场景(实测) | 响应体特征 |
|---|---|---|
| 200 OK | 会话查询、列表/单条读取、PUT 成功 | JSON;写操作回新 ETag |
| 201 Created | POST 创建成功 | 新资源 JSON + ETag 头 |
| 400 Bad Request | PUT 请求体缺必填字段(如 id) | JSON Schema 校验结构(error/errors/msg) |
| 401 Unauthorized | 无凭据/密码错/账套不存在/伪造 company_path | 单行 Credentials invalid or session expired |
| 403 Forbidden | DELETE 且携带 If-Match | 空体;删除被设计性拒绝 |
| 404 Not Found | 集合路由缺尾斜杠(/goods)、不存在的文档端点(/doc) | 空体 |
| 409 Conflict | PUT 携带过期 If-Match | 空体;重新 GET 再重试 |
| 500 Internal Server Error | PUT 缺 If-Match、PATCH 任意形态、DELETE 缺 If-Match | 单行 Internal Server Error;查服务端日志定位 |
| 501 Not Implemented | invoices、orders 等未开放域 | 空体 |
九、端到端脚本示例
把上述实测串成一个可直接复制的集成脚本(bash + curl,逐行对应本文实测命令):
#!/bin/bash
# LedgerSMB 1.13.8 API 集成最小流程(2026-10-24 实测通过)
BASE=http://localhost:8130
COMPANY=demo1
CP=e368b9938746fa090d6afd3628355133 # md5(公司名);也可从认证响应 target 字段解析
API=$BASE/$CP/erp/api/v0
JAR=$(mktemp)
# 1) 种匿名会话 Cookie
curl -s -c $JAR $BASE/login.pl > /dev/null
# 2) 认证(响应体 target 字段即含 company_path)
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$BASE/login.pl?action=authenticate" \
-d '{"login":"admin","password":"q1w2e3r4","company":"demo1"}'
# 3) 确认会话与权限
curl -s -b $JAR $API/session | python3 -m json.tool
# 4) 读列表(每条内嵌 _meta.ETag,可直接用于 If-Match)
curl -s -b $JAR $API/products/partsgroups/
# 5) 创建
ETAG=$(curl -s -b $JAR -D - -o /dev/null -H 'Content-Type: application/json' \
-X POST $API/products/partsgroups \
-d '{"description":"集成新建组"}' | grep -i '^etag' | tr -d '\r' | sed 's/^[Ee]tag: *//')
# 6) 乐观锁更新(必须带 If-Match;失败得到 409 则回到第 4 步重取)
curl -s -b $JAR -D - -H 'Content-Type: application/json' \
-H "If-Match: $ETAG" -X PUT $API/products/partsgroups/4 \
-d '{"id":4,"description":"集成更新后的组"}'
脚本要点:会话全程依赖 Cookie Jar;每次写操作后 ETag 都在变,务必从最近一次写响应头取值;409 是"重取再试"的信号而非致命错误。
十、安全与生产注意
- company_path 不是密码。MD5 挂载前缀只是路径混淆(URL 不暴露库名),401 矩阵已证明伪造前缀照样被拒。不要在前端代码里把它当作秘密——认证只认账号、密码与 Cookie。
- 会话 Cookie 强度。
HttpOnly+SameSite=Strict已由服务端强制,客户端脚本拿不到会话;但生产环境仍必须上 HTTPS(呼应介绍篇的 Safari 限制与部署篇的 TLS 结论),否则 Cookie 本体在链路上明文可截。 - 密码过期感知。
session端点的password_expiration是做"改密提醒"的官方数据源;集成系统应消费该字段而不是让用户撞到登录失败才反应。 - 500 排障路径。遇到 500 先看服务端日志(本文 PATCH 的根因即从日志行
Router.pm line 501定位),不要靠盲试参数;1.13.8 的 500 多为"能力未就绪"而非配置问题。 - 权限同源。API 不提供超权通道:
roles清单就是权限天花板。为集成建专用账号时,在 Web UI 中按最小权限授予角色,API 侧自动受限。
十一、参考链接
- 官网:https://ledgersmb.org/
- GitHub 仓库(1.13 分支,API 路由层源码
lib/LedgerSMB/Router.pm、lib/LedgerSMB/PSGI.pm、lib/LedgerSMB/Routes/ERP/API/):https://github.com/ledgersmb/LedgerSMB - 1.13.8 发布页:https://github.com/ledgersmb/LedgerSMB/releases/tag/1.13.8
- 官方下载站点(实查当前最新为 1.13.8,2026-10-24):https://download.ledgersmb.org/f/Releases/
- 官方在线手册:https://book.ledgersmb.org/
- 本专栏同系列:产品认知见《LedgerSMB 1.13.8 介绍与选型指南》;部署实操见《LedgerSMB 1.13.8 Docker Compose 部署指南》;配置项精读见《LedgerSMB 1.13.8 配置参数详解》。