Open QuarterMaster 6.5.0 REST API 实战:从认证到库存事务
Open QuarterMaster Core API 全链路实测:OpenAPI 端点、Keycloak 认证、存储块/物品/入库事务逐条 curl 取证,含三次字段踩坑与 MongoDB 副本集排障实录(2026-10-24)。
Open QuarterMaster 6.5.0 REST API 实战:从认证到库存事务
Open QuarterMaster 的所有库存能力都建在 Core API 的 REST 接口上——Web 前端 Base Station 本身就是这套 API 的消费者。想把它接进自动化流程(ERP 同步、扫码枪入库、报表拉数),直接调 API 是正路。本文全部接口均在实测环境(Core API 6.5.0,2026-10-24)用 curl 逐条跑通,回显原样收录,包括三次踩坑的完整排障过程。
一、API 基址与 OpenAPI 规范
按部署篇的 Compose 形态,Core API 独立端口监听(实测环境为 8139,官方默认编排为 8081)。单机 Captain 部署则统一从 80/443 经 Traefik 进入。
规范文档端点实测:
curl -o /dev/null -w "%{http_code}\n" http://localhost:8139/openapi
# 实测回显:404
curl -o openapi.json http://localhost:8139/q/openapi
# 实测回显:HTTP 200,完整 JSON 规范
注意路径带 /q 前缀(Quarkus 惯例),不带前缀的 /openapi 是 404。生产镜像不内置 Swagger UI 页面(/openapi-ui/index.html 实测 404),只提供裸规范,需要可视化的话把 /q/openapi 的 JSON 导入任意 OpenAPI 渲染器即可。
规范规模(实测解析):78 个端点,按 tag 分布如下:
| 域 | 端点数 | 说明 |
|---|---|---|
| Inventory Items | 21 | 物品 CRUD、事务、stored 明细 |
| Storage Blocks | 10 | 存储块 CRUD、内容列举 |
| Item Categories | 9 | 物品分类 |
| Inventory Management | 9 | 库存库(DB)管理与导入导出 |
| File Attachments | 9 | 附件上传下载 |
| Media | 11 | 图片/媒体 |
| Units | 8 | 计量单位与换算 |
| ID Generation | 7 | 物品编号生成器 |
| Item Checkout | 6 | 借出/归还 |
| Interacting Entities | 4 | 操作者实体(用户/设备) |
| Informational | 1 | 系统信息 |
| MicroProfile Health | 4 | 健康检查(/q/health 等) |
二、认证:从 Keycloak 拿 Bearer Token
Core API 走 OIDC/JWT(Keycloak,realm=oqm,客户端 oqm-app)。oqm-app 客户端默认开启 direct access grants,支持 OAuth2 password grant(realm 配置见配置篇):
curl -sS -X POST "http://localhost:8138/realms/oqm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&client_id=oqm-app&client_secret=**********&username=admin&password=Q1w2e3r4!"
实测回显(截取):access_token(JWT)、expires_in: 900、token_type: Bearer。解析 token 的 payload 可以看到角色已带上(实测):
"realm_roles": ["default-roles-oqm", "inventoryAdmin", "inventoryEdit", "inventoryView", "itemCheckout", "user"]
不带 token 调受保护接口的对照(实测回显):
HTTP/1.1 401 Unauthorized
{"displayMessage":"Unauthorized"}
之后所有请求带 Authorization: Bearer <access_token>。
两个认证侧的坑(实测确认):
client_secret的值就是 realm 导入文件里的字面星号串**********——不是占位符,是真实值。- 用户密码受 Keycloak 策略约束(大小写+数字+特殊字符+8 位),
q1w2e3r4会在设置密码时被拒(HTTP 400,回显见配置篇第五节)。
三、多库存库:/api/v1/db/{oqmDbIdOrName}/ 路径约定
OQM 支持多个独立库存库。绝大多数业务端点都挂在 /api/v1/db/{oqmDbIdOrName}/ 下,{oqmDbIdOrName} 填库存库的名字或 ID。查当前有哪些库(实测回显):
curl -sS http://localhost:8139/api/v1/inventory/manage/db -H "Authorization: Bearer $UT"
# 实测回显:["default"]
全新实例自带 default 库。新建库用 POST /api/v1/inventory/manage/db(Body 为库名)。
先认识"操作者":每次库存操作都会记录是谁做的(实测回显):
curl -sS http://localhost:8139/api/v1/interacting-entity/self -H "Authorization: Bearer $UT"
# 实测回显:
# {"id":"6adc0c2e01d5893b9b4731fe","type":"user","attributes":{},"keywords":[],"username":"admin",...}
这个 id 就是后续所有事务流水里的 entity 字段。
四、建数据:存储块、分类、物品(含三次踩坑实录)
4.1 存储块(Storage Block)
请求体里 label 与 location 是必填字段(漏掉会 400,实测回显:Data validation errors - label must not be null, location must not be null);type 用 {"strType":{"type":"SHELF"}} 结构,可选类型含 BOX/SHELF/ROOM/PALLET/CABINET 等:
curl -sS -X POST "http://localhost:8139/api/v1/db/default/inventory/storage-block" \
-H "Authorization: Bearer $UT" -H "Content-Type: application/json" \
-d '{"label":"Shelf A","nickname":"一号架","description":"实验一号货架","location":"仓库 1 层","type":{"strType":{"type":"SHELF"}},"attributes":{},"keywords":["rack"]}'
# 实测回显(HTTP 200):{"id":"6adc0c5301d5893b9b473201","label":"Shelf A","nickname":"一号架",...,"type":{"strType":{"type":"SHELF"}},...}
记下返回的 id(24 位 Mongo ObjectId 形态),后面反复用到。
4.2 物品分类(Item Category)
curl -sS -X POST "http://localhost:8139/api/v1/db/default/inventory/item-category" \
-H "Authorization: Bearer $UT" -H "Content-Type: application/json" \
-d '{"name":"电阻","description":"贴片电阻类","attributes":{}}'
# 实测回显(HTTP 200):{"id":"6adc0c3a01d5893b9b4731ff","name":"电阻","description":"贴片电阻类",...}
注意:分类用 name 字段,存储块用 label——两套字段名不一致,是 OQM API 的第一个小坑。
4.3 物品(Inventory Item):三次踩坑实录
物品是全 API 里字段最复杂的对象。按 OpenAPI 的 InventoryItem schema,name、storageType、identifiers、idGenerators、defaultPrices、associatedLinks、unit 为必填。以下是实测三次失败后跑通的正确写法:
坑一:storageBlocks 里的引用要传裸 hex 字符串。第一次按直觉传 {"storageBlock":{"id":"<hex>"}},服务端 400:
com.fasterxml.jackson...: hexString can not be null
(through reference chain: InventoryItem["storageBlocks"]->ArrayList[0]->StorageBlockSettings["storageBlock"])
storageBlock 字段的 schema 是 ObjectId 类型,直接传 24 位 hex 字符串,不要包一层 {"id":…}。
坑二:storageType 是裸字符串枚举。改用 {"strType":{"type":"BULK"}} 结构(存储块 type 的同款写法)后,服务端 500,日志显示:
Cannot deserialize value of type `StorageType` from Object value
物品的 storageType 与存储块的 type 虽然同源于 StorageType,但物品侧要的是裸字符串 "BULK"(可选 BULK/AMOUNT_LIST/UNIQUE_MULTI/UNIQUE_SINGLE)。
坑三:两个 500 的表象相同,根因要靠 Core API 日志定位。HTTP 层只给 500 - Internal Server Error 和一个 error id,真正原因(Jackson 反序列化失败、还是 MongoDB 操作失败)要用 error id 去 docker logs 里搜。
最终跑通的请求(实测回显 HTTP 200):
curl -sS -X POST "http://localhost:8139/api/v1/db/default/inventory/item" \
-H "Authorization: Bearer $UT" -H "Content-Type: application/json" \
-d '{
"name": "10k 0805 Resistor",
"description": "1/8W SMD resistor",
"storageType": "BULK",
"identifiers": [],
"idGenerators": [],
"defaultPrices": [],
"associatedLinks": [],
"categories": ["6adc0c3a01d5893b9b4731ff"],
"storageBlocks": [{"storageBlock": "6adc0c5301d5893b9b473201", "notificationStatus": {"lowStock": false}}],
"unit": {"string": "units"},
"attributes": {},
"keywords": ["resistor", "0805"]
}'
# 实测回显(HTTP 200):
# {"id":"6adc0cf501d5893b9b473203","attributes":{},"keywords":["resistor","0805"],"imageIds":[],
# "name":"10k 0805 Resistor","storageType":{"strType":{"type":"BULK"}},"description":"1/8W SMD resistor",...}
注意回显里 storageType 又变成了 {"strType":{"type":"BULK"}}——请求收裸字符串、响应回结构体,写客户端解析时要按响应结构来。
五、库存事务:入库流水(ADD_AMOUNT)
事务端点:POST /api/v1/db/{oqmDbIdOrName}/inventory/item/{itemId}/stored/transaction。OpenAPI 定义了 12 种事务类型(ADD_AMOUNT、ADD_WHOLE、SET_AMOUNT、SUBTRACT_AMOUNT、TRANSFER_AMOUNT、CHECKOUT_AMOUNT 等),type 字段决定请求体其余字段。
连续两笔入库实测(500 + 250):
curl -sS -X POST "http://localhost:8139/api/v1/db/default/inventory/item/$ITEM/stored/transaction" \
-H "Authorization: Bearer $UT" -H "Content-Type: application/json" \
-d '{"type":"ADD_AMOUNT","toBlock":"6adc0c5301d5893b9b473201","amount":{"value":500,"unit":{"string":"units"},"scale":"ABSOLUTE"}}'
# 实测回显(HTTP 200,截取):
# {"id":"6adc0df448f891c52ebea867","entity":"6adc0c2e01d5893b9b4731fe",
# "inventoryItem":"6adc0cf501d5893b9b473203","affectedStored":["6adc0df448f891c52ebea868"],
# "timestamp":"2026-10-24T01:46:29.002580085Z[GMT]",
# "transaction":{"toBlock":"6adc0c5301d5893b9b473201","amount":{"unit":{"string":"units","name":"Units","symbol":"units"},"scale":"ABSOLUTE","value":500},"type":"ADD_AMOUNT","schemaVersion":1},
# "postApplyResults":{"expiryLowStockResults":{...}}}
第二笔 250 同样 200。事务流水记录了:操作者(entity)、物品、受影响的 stored 记录、时间戳、原始事务参数,以及事务应用后的告警计算结果(postApplyResults,过期/低库存检查)。
排障实录:事务接口 500 与 MongoDB 副本集。最初两笔事务全部 500,HTTP 层只回 500 - Internal Server Error。拿 error id 去 Core API 日志里搜到根因(实测回显):
com.mongodb.MongoQueryException: Command execution failed on MongoDB server with error 20
(IllegalOperation): 'Transaction numbers are only allowed on a replica set member or mongos'
Core API 的库存事务使用了 MongoDB 多文档事务,单机(standalone)模式的 MongoDB 不支持,必须以副本集运行。处理:mongo 以 --replSet rs0 --keyFile <keyfile> 启动并 rs.initiate(),Core API 连接串加 ?replicaSet=rs0&authSource=admin(完整步骤见部署篇 5.2 节)。改造后同一请求立即 200。这也是 Captain 单机部署替你处理掉的最大一个隐藏依赖。
六、核对库存:stored 明细
事务是否生效,查物品的 stored 明细(实测回显,截取):
curl -sS "http://localhost:8139/api/v1/db/default/inventory/item/$ITEM/stored" -H "Authorization: Bearer $UT"
# 实测回显(HTTP 200,截取):
# {"results":[{"id":"6adc0df448f891c52ebea868","item":"6adc0cf501d5893b9b473203",
# "state":{"storageBlock":"6adc0c5301d5893b9b473201","type":"STORED"},
# "notificationStatus":{"expired":false,"expiredWarning":false,"lowStock":false},
# "labelText":"750 units","amount":{"unit":{...},"value":750.000000,...}}]}
500 + 250 = 750 units,labelText 是系统为该 stored 记录生成的展示文案。BULK 类型物品的在库数量聚在这里;低库存/过期判定状态在 notificationStatus。
七、出库与盘点:换 type 就行
同一端点换 type 字段即可完成其余操作,常用请求体模板(字段均出自 OpenAPI schema):
// SUBTRACT_AMOUNT:减数量
{"type":"SUBTRACT_AMOUNT","fromBlock":"<blockId>","amount":{"value":50,"unit":{"string":"units"},"scale":"ABSOLUTE"}}
// SET_AMOUNT:盘点设定为绝对值
{"type":"SET_AMOUNT","atBlock":"<blockId>","amount":{"value":700,"unit":{"string":"units"},"scale":"ABSOLUTE"}}
// TRANSFER_AMOUNT:块间转移
{"type":"TRANSFER_AMOUNT","fromBlock":"<blockA>","toBlock":"<blockB>","amount":{"value":100,"unit":{"string":"units"},"scale":"ABSOLUTE"}}
// CHECKOUT_WHOLE:整件借出(UNIQUE 类物品)
{"type":"CHECKOUT_WHOLE","fromBlock":"<blockId>"}
参数名前缀有规律:to(进)、from(出)、at(设定目标)。写自动化前先用 GET /q/openapi 对照当前版本的字段定义,schema 演进较快。
八、单元换算与编号生成
- Units 域(8 端点):查兼容单位与换算系数。OQM 内置数量/长度/质量等维度,
unit在请求里用{"string":"units"}的简写形式,服务端会补全成{"string":"units","name":"Units","symbol":"units"}(见事务回显)。 - ID Generation 域(7 端点):为物品定义自动编号规则(idGenerators),配合标签打印使用。
九、速查:从零到有库存的调用序列
1. POST /realms/oqm/protocol/openid-connect/token # Keycloak 拿 token(password grant)
2. GET /api/v1/interacting-entity/self # 确认身份,拿到 entity id
3. GET /api/v1/inventory/manage/db # 列库存库(默认 ["default"])
4. POST /api/v1/db/default/inventory/storage-block # 建存储块(label+location 必填)
5. POST /api/v1/db/default/inventory/item-category # 建分类(name 字段)
6. POST /api/v1/db/default/inventory/item # 建物品(storageType 裸字符串、storageBlock 裸 hex)
7. POST /api/v1/db/default/inventory/item/{id}/stored/transaction # 入库/出库/转移(须 Mongo 副本集)
8. GET /api/v1/db/default/inventory/item/{id}/stored # 核对在库明细
本专栏其他文章
- Open QuarterMaster 6.5.0 介绍与选型指南
- Open QuarterMaster 6.5.0 部署指南:Station Captain 单机安装与 Compose 编排
- Open QuarterMaster 6.5.0 配置参数详解
参考来源
- OpenAPI 规范:实测
GET /q/openapi导出(Core API 6.5.0,78 端点),2026-10-24 - 全部请求/响应回显:实测环境逐条 curl 取证(Core API 6.5.0 + Keycloak 26.5 + MongoDB 6 副本集),2026-10-24
- 排障日志:Core API 容器日志(error id 定位法),2026-10-24