Open QuarterMaster 6.5.0 REST API 实战:从认证到库存事务

Open QuarterMaster Core API 全链路实测:OpenAPI 端点、Keycloak 认证、存储块/物品/入库事务逐条 curl 取证,含三次字段踩坑与 MongoDB 副本集排障实录(2026-10-24)。

Open QuarterMasterREST API库存管理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 Items21物品 CRUD、事务、stored 明细
Storage Blocks10存储块 CRUD、内容列举
Item Categories9物品分类
Inventory Management9库存库(DB)管理与导入导出
File Attachments9附件上传下载
Media11图片/媒体
Units8计量单位与换算
ID Generation7物品编号生成器
Item Checkout6借出/归还
Interacting Entities4操作者实体(用户/设备)
Informational1系统信息
MicroProfile Health4健康检查(/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>。

两个认证侧的坑(实测确认):

  1. client_secret 的值就是 realm 导入文件里的字面星号串 **********——不是占位符,是真实值。
  2. 用户密码受 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    # 核对在库明细

本专栏其他文章

参考来源

  • 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