ERP 与系统集成指南

动手之前先界定集成范围:API 能触及什么、不能触及什么,边界在哪里

阅读时间 9 分钟
打印本章节

本指南面向正在为 FreeMaint 与另一套系统规划集成的开发者——对方可能是 ERP、SCADA 历史库、仓储或财务软件。本节的其他文章讲的是怎么调用,这一篇刻意只谈边界,因为代价高昂的失误从来不是写错一个请求头,而是做到一半才发现自己想当然的某项能力根本不存在。下面列出的每一条限制,都是有意写明的。

一把密钥,三扇门

API 密钥(以 fmk_ 开头)由管理员在「公司设置 > API」(/company-settings/api)中创建,或由人员在「个人资料 > 安全 > API 密钥」中为自己创建,并以 Authorization: Bearer fmk_... 的形式发送。它恰好只认证三样东西:位于 POST /api/v1/graphql 的 GraphQL 端点、能耗数据接入端点,以及 /public-api/whoami 和 /public-api/ping。它不认证 /api/v1/{resource} 下的 REST 路由——那些是应用自身的 API,需要用户会话(JWT)。请把集成建在 GraphQL 上:那才是密钥能触及的接口面。

API 密钥能给你什么

明文密钥只在创建时显示一次,之后无法再找回:系统只保存 SHA-256 哈希,外加 fmk_ 之后的 6 个字符,用于在列表里辨认这把密钥。

名称

给人看的标签——「Oracle ERP 桥接」「2 号线看板机」。每一个接入系统单独发一把密钥,这样吊销其中一把时不会影响其他。

访问级别

READ、READ_STOCK 和 FULL 构成一个在所有者权限之上的有序层级。READ 只能执行查询,是默认值。READ_STOCK 在此基础上增加 adjustStock 变更。FULL 允许所有者角色允许的一切操作,包括创建、更新和删除——当所有者有权创建能源读数时,还包括能源数据接入端点。

有效期

可选的 expiresAt。过了这一刻,密钥便不再通过认证。长期运行的服务集成留空即可;给外包商或试点项目则设定一个日期。

吊销

isActive 是每把密钥各自独立的开关,几秒内生效。记录会保留以备审计,因此被吊销的密钥仍然可追溯,而不是凭空消失。

最近使用

lastUsedAt 记录最近一次通过认证的调用——足以发现一把已经没人再用的密钥。

所有者

密钥所代表的那个人。每次调用都带有其权限和数据可见范围,密钥写入的内容也以其姓名署名。对于 ERP,请创建一个专用用户——“ERP 集成”——为其分配一个恰好覆盖桥接所需权限的角色,再把密钥交给它。

API 密钥不能给你什么

以下是当前实现的真实限制,不是本页的疏漏。如果你的安全评审依赖其中任何一条,请现在就据此设计,而不是等系统建完再说。

  • 没有按密钥划分的模块列表。密钥能读写的内容就是其所有者角色所允许的——如需收窄,请把它交给角色更窄的所有者。在此之上只适用三个访问级别:你无法在密钥本身上列出它可以使用的模块。
  • 没有 IP 白名单。密钥在互联网上任何地方都有效。请把它当作机密对待:放进密钥保管库或平台的机密存储,绝不要放进代码仓库、共享盘或工单里。
  • 没有按密钥的使用日志。系统只保留 lastUsedAt。没有按密钥的调用历史,没有按密钥的配额,也没有任何记录说明哪把密钥执行了哪一次操作。每日配额计数器是按公司计的,所有密钥共用,因此一个失控的集成足以耗尽其他所有集成的配额。
  • 不能代人审批。即使所有者本人可以审批,密钥也绝不会审批工作流程步骤:审批始终是应用中由人完成的操作。
  • 一把密钥只对应一家公司。密钥绑定在创建它的公司上,无法访问其他公司的数据:其他公司的记录根本查不到。

你能读到什么

GraphQL 提供 16 个读取查询:whoami、workOrders、workOrder(id)、assets、asset(id)、assetDowntimes、meters(assetId)、parts、part(按 id 或条码)、stockMovements、locations、vendors、customers、purchaseOrders、purchaseOrder(id | reference | erpReference) 和 floorPlans(locationId)。每个列表都可以一直翻到末尾:workOrders、assets、assetDowntimes、meters、parts、purchaseOrders、locations、vendors 和 customers 都接受 limit(1 到 200)加 offset,其中 workOrders、assets、parts、purchaseOrders、locations、vendors 和 customers 还接受用于增量同步的 updatedSince。默认每页 50 行,但 locations、vendors 和 customers 保留原来的默认值 200。每个列表都以 id 作为并列时的次级排序键,因此翻页边界既不会漏掉也不会重复任何一行。stockMovements 是你据以对账的库存流水账:它按从旧到新的顺序读取,并用 afterId 翻页——传入你收到的最后一条变动的 id——即使有新的变动正在写入,结果也保持准确;since 则让你从某个日期开始回填历史数据。回收站中的工单永远不会返回。purchaseOrders 还可按 status 和 vendorId 过滤;金额使用公司币种,每个订单都在 currency 字段中带出。

一次调用即可验证密钥并读取工单: curl -X POST https://freemaint.com/api/v1/graphql -H "Authorization: Bearer $FMK" -H "Content-Type: application/json" -d '{"query":"{ whoami { companyId tier } workOrders(limit: 5, status: \"OPEN\") { id reference title status priority dueDate } }"}'

你能写入什么

共十二个 mutation:对工单、资产和备件的创建、更新与删除,另加用于带符号库存变动的 adjustStock。它们走的是应用自身使用的同一套服务,因此编号(WO-…、ASS-…、PART-…)、审计记录和通知的表现与在界面中完全一致。库存操作请优先使用 adjustStock:它会在同一个事务里写入一条 StockMovement 审计记录,并拒绝把数量降到零以下;而 updatePart(quantity) 设置的是绝对值,并把差额记录为一次调整。对于按位置管理库存的备件,请向 adjustStock 传入 locationId;在这类备件上,updatePart(quantity) 会被拒绝。第十一个 addMeterReading 通过与计量表页面相同的服务记录计量表读数,因此计量表触发器和基于计量表的预防性维护的反应与在应用中录入读数时完全一致。第十二个 setPurchaseOrderErpReference 把您 ERP 的单据编号保存到采购订单上,以便日后用 purchaseOrder(erpReference: …) 将两条记录对应起来。它需要 FULL 权限的密钥,并且有意不触发任何 webhook:您的 ERP 永远不会以事件形式收到自己的写入。

API 完全没有覆盖的部分

GraphQL 模式包含工单、资产、设备停机、备件及其库存变动、计量表、采购订单、位置、供应商、客户和楼层平面图。除此之外没有别的。以下是 FreeMaint 中完全可用、但今天没有任何 API 接口的部分:无法从集成侧读取、写入或订阅,现有查询的任何组合都触及不到它们。

  • 巡检——模板、排定的执行、记录下来的结果、动作代码,一个都没有。既没有巡检查询,也没有巡检 mutation,更没有巡检的 webhook 事件。
  • 用户、角色和团队——您无法通过这套 API 列出、创建或停用用户。用户开通在应用内完成;在 Enterprise 方案中可以用 SCIM 自动化,但那是另一套独立机制,不属于本 API。
  • 项目——没有查询,也没有 mutation。
  • 采购申请——没有查询,也没有 mutation。采购订单可以读取(purchaseOrders、purchaseOrder),您的 ERP 可以回写自己的编号(setPurchaseOrderErpReference),purchase_order.* webhook 也会通知订单变化;但无法通过 API 创建、修改或收货采购订单,这些操作仍在应用中完成。
  • 文件、照片和附件——你既不能上传文档或照片,也无法把它们读回来。工单照片、备件图片和巡检凭证都在 API 之外。响应里出现的 URL,比如备件的 imageUrl,并不是上传路径。
  • 预防性维护计划、工时记录、任务和检查清单——均未暴露。

重要

请按这份清单来界定开发范围,而不是按产品在浏览器里能做什么来判断。FreeMaint 自己的网页端和移动端跑在一套 API 密钥无法触及的内部 REST API 上,所以某个功能在界面上看得见,并不能证明集成能够驱动它。

Webhook——事件目录

Webhook 是集成的「推送」那一半:不需要轮询,事情一发生 FreeMaint 就会向你的 URL 发起 POST。在「公司设置 > Webhooks」(/company-settings/webhooks)中配置,并为每个 webhook 订阅你需要的事件。事件目录是固定的。

  • work_order.created — 工单已创建
  • work_order.completed — 工单进入「已完成」
  • work_order.status_changed — 工单切换到任意状态;载荷带有 new_status。它与保持不变的 work_order.completed 同时触发
  • request.created — 提交了一条维修申请
  • request.status_changed — 申请被批准、驳回或取消
  • part.low_stock — 备件降到最低库存或以下
  • part.out_of_stock — 备件降到零;这是独立事件,不是 low_stock 的变体
  • part.low_stock_summary — 每家公司每天一份汇总,列出处于再订货点或以下的备件,包括从未发生过出入库的备件
  • part.out_of_stock_summary — 同一份每日汇总,针对库存为零的备件
  • part.consumed — 备件在工单上被消耗,或从工单退回;quantity 带符号
  • compliance.chain_divergence — 审计哈希链断裂(Enterprise)
  • asset.downtime_started — 开启了一次设备停机
  • asset.downtime_ended — 关闭了一次设备停机;现在可以核算时长
  • report.daily_summary — 当天已完成的工作,每天一次,时间由你设定
  • purchase_order.created — 创建了一张采购订单;负载包含订单(reference、status、total_cost、currency、expected_date、erp_reference)及其供应商
  • purchase_order.status_changed — 采购订单状态发生变化(已批准、已下单、已取消……);负载包含 previous_status 和 new_status
  • purchase_order.received — 某张采购订单收到了一次到货(每次到货都会发送,包括部分到货);负载列出每一行的 quantity_ordered、quantity_received(本次到货)、quantity_received_total 和 quantity_remaining,以及 receipt_id、fully_received 和 closed_short;仍有货物待到时 new_status 为 PARTIALLY_RECEIVED,订单完成或关闭后为 RECEIVED

警告

没有巡检事件,也没有缺陷或发现项事件。如果你的流程是「巡检不合格,我的 ERP 就响应」,今天没有任何事件会为此触发。请把这个缺口明确地纳入方案,而不要假设存在一个足够接近的事件。

投递信封

每一次 GENERIC 投递都是一个 POST,请求体始终是同一个三键信封:event、timestamp 和 data,其中 data 按实体命名。随行的还有三个请求头。X-FreeMaint-Signature 的格式为 t=<unix 秒>,v1=<hex>,其中 v1 是以该 webhook 的密钥,对精确字符串“<t>.<原始请求体>”做的 HMAC-SHA256;X-FreeMaint-Event 重复事件名;X-FreeMaint-Delivery-Id 标识这次投递。请在解析之前,针对原始请求体校验签名——重新序列化 JSON 会改变字节,签名就对不上了。

{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "3号泵泄漏", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }

可靠性与监控

一次投递最多尝试 3 次,采用以 2 秒为基数的指数退避——大约 2 秒、4 秒、8 秒——每次尝试 10 秒后超时。返回 2xx 即标记为已投递。5xx 响应和网络错误会重试。4xx 被视为永久性拒绝,完全不会重试:如果你的端点因为自己这边的 bug 返回 400 或 401,这次投递就到此为止,只有手动重发才能补回来。

  • 每一次投递都会存档:事件、实际发送的负载、状态、你返回的 HTTP 码、你响应体的前约 500 个字符、尝试次数和错误信息。
  • 状态有 PENDING、DELIVERED、FAILED、RATE_LIMITED(已达到 Starter 每天 100 次投递的上限)和 SKIPPED_TIER(当前套餐不再包含该目标)。
  • 存档的投递可以从 webhook 的历史记录中重发。它会原样重新发送已保存的负载,因此其中的业务主键保持不变,正确做了去重的接收方不会重复入账。
  • 每个 webhook 都带有 lastTriggeredAt、lastSuccessAt、lastFailureAt 和 failureCount,不必打开完整历史就足以做健康检查。
  • webhook 列表及其投递日志在所有套餐下都可读,因此因降级而停摆的集成是看得见的,而不是无声死亡。
  • 投递开始失败时不会有任何提醒——没有邮件,也没有通知。如果无声故障对你有影响,请从你自己的监控系统轮询投递日志。

安全

保存 webhook 时会校验 URL,规则严格是有意为之:webhook 是由我们的服务器发出的出站请求,规则一旦宽松就会变成 SSRF 攻击面。

  • 密钥以 SHA-256 哈希存储——创建之后无法再从 FreeMaint 还原出明文。
  • Webhook URL 必须使用 https。普通 http 以及其他任何协议都会被拒绝。
  • 只接受 443 和 8443 端口。
  • 回环地址、链路本地地址和私有网段主机(127.x、10.x、192.168.x、172.16–31.x、::1 之类)都会被拒绝:接收端必须能从公网访问。只存在于你厂区内部的系统需要一个公网中继或隧道。
  • URL 中带凭据(https://user:pass@host)会被拒绝。
  • 不跟随重定向——你注册的那个端点就是收到 POST 的端点。
  • 每个 webhook 都有自己的签名密钥,并提供轮换密钥的操作。轮换立即生效,所以请在同一时间窗口内把新密钥部署到你这边。
  • SLACK 和 TEAMS 目标收到的是按平台格式化的消息,并且不带 FreeMaint 的 HMAC 请求头:对它们而言,URL 的保密性就是唯一的认证手段。凡是需要密码学验证的场景,请使用 GENERIC。

今天能跑得很顺的模式

以下是当前接口面能干净支持的集成,以及让每一种都能安全跑在生产环境的关键细节。

把消耗同步进 ERP

订阅 part.consumed。quantity 带符号——发料到工单时为负,退回时为正——负载中还带有 stock_movement_id,它在重试和重发之间保持不变,请用它作为幂等键。adjustStock mutation 刻意不会触发这个事件,正是这一点让你自己写进来的库存变动不会回音式地传回给你,形成死循环。

把库存修正推进来

从你这一侧调用 adjustStock 并传入带符号的增量。READ_STOCK 密钥能够完成这件事,却无法创建、更新或删除任何内容,因此非常适合单向库存桥接。

从其他系统创建工单

根据 SCADA 报警、服务台工单或 ERP 的故障通知调用 createWorkOrder。工单会拿到正常的编号,并像其他工单一样触发 work_order.created。

让备件目录保持同步

parts(updatedSince:) 只返回你指定时刻之后发生变化的记录。createPart 和 updatePart 接受 customReference——你自己给这个备件的编码——随后 part(barcode:) 就能按它匹配,于是你可以用 ERP 里已有的标识去查询备件。系统不强制它唯一;唯一性要由你来保证。

在完成时闭环

work_order.completed 携带该工单及其此前的状态,足以在另一侧关闭一条维修通知。

需要什么套餐

两道彼此独立的门槛:一道针对 API,一道针对 webhook。使用 Starter 方案的公司无需 API 即可接收 webhook。

GraphQL API 与 API 密钥

Business 及以上。Core 和 Starter 根本无法创建密钥。

每日配额

Business 每天 5,000 次调用,Enterprise 100,000 次。按公司、按操作计数,在 UTC 00:00 重置——把多个操作合并进一个 HTTP 请求只能省下网络往返,永远省不下配额。

Webhook

Starter 及以上,上限为每天 100 次投递;从 Business 起不限量。

Slack 与 Teams 目标

Enterprise。通用 webhook 从 Starter 起即可使用。

提示

打算做的方案正好撞上上面某个缺口?把你要构建的东西告诉我们:contact@freemaint.com。下一个交付哪项能力,取决于客户实际卡在哪里。

本页对您有帮助吗?