干将智联 / 开放 API 接入说明 / V1.0 · 2026-09-22 ← 返回首页

开放 API · AccessKey

把机柜数据拉进你自己的系统

你的系统拿着一把 AccessKey,就能按需拉取名下全部机柜的实时数据 —— 温度、烟雾、水浸、在线状态。只读,不消耗短信余额, 也不需要我们在服务端为你写任何代码。消防主机、ERP、自建看板都能直接接。

数据流向 · 由你主动拉取 YOUR SYSTEM → GANJIANG
01 · 发起方 你的系统 定时发 HTTPS GET
02 · 数据源 干将服务器 实时快照 · 只读
03 · 现场 机柜设备 烟雾 / 水浸 / 温度

每把 key 一个 30 秒窗口,窗口内再调就是 429

距上次 ≥ 30 秒放行 恰好 30 秒整放行 差 1 微秒429 + Retry-After
01 01 — 概念

这是什么What it is

这是平台对外的一小组只读接口。你的系统主动来拉,我们被动应答 —— 拉哪些设备、多久拉一次,都由你的代码决定。

告警回调(Webhook)开放 API(本文档)
方向我们给你我们
触发设备告警时立即你调用时
内容一条告警事件全部设备的实时快照
需要一个公网可访问的地址一把 AccessKey
适合事件驱动:立刻派单、打电话定时巡检:画大屏、抄表、留档

两者可以同时用,互不影响。常见组合:用回调做「立刻响应」, 用开放 API 做「每 30 秒刷新一次的看板」。

开权限就能用

开放 API 不需要在服务器上做任何配置,也没有独立开关。你的账号下有设备、 建了一把启用的 AccessKey,就能立刻调用。

02 02 — 第一步

拿到 AccessKeyGet your key

在手机 App 里自助创建,不需要联系我们。每把 key 可以单独命名 (「消防平台」「财务看板」),也可以单独停用或删除 —— 一个集成出问题, 停它一把不影响别的。

创建后会立刻得到两样东西:

名称样子怎么对待
AccessKey ID gj_ak_3f9a2b7c1d8e4a5b 相当于用户名。可以放在日志、配置里,泄露了也不要紧
AccessKey Secret kJ8xQw3fR7mN2pL5tZ9vB1cD4eF6… 相当于密码,43 个字符。泄露等于别人能读你全部设备数据
⚠️ secret 只显示一次

创建成功后的那个弹窗,是你唯一一次看到 secret 的机会。 关掉之后,任何接口、任何人(包括我们)都再也取不回它 —— 数据库里只存了它的哈希值。

请当场复制到你的配置或密钥管理里。找不回就只能删掉重建一把, 然后用新 secret 重新配置你的系统。

与阿里云的 AccessKey 无关

如果你同时在对接阿里云短信或语音,请注意:本平台的 AccessKey 与阿里云的 AccessKey 没有任何关系,两者不能混用、也不通用。名字撞车纯属巧合 —— 填的时候认准本平台 App 里生成的那一对。

别重复点「创建」

创建接口不是幂等的:每点一次就多一把新 key。手抖双击、或请求超时后 重试,会留下两把同名的 key —— 其中一把的 secret 谁也看不到(它只出现在那次 没送达的响应里),而且两把都占配额。超时了先刷新列表看看,再决定要不要重试。

03 03 — 地址

接口地址Endpoints

一共两个接口,都是 GET,都走 HTTPS:

endpoints
# 名下全部设备(推荐:设备多的时候一次拿完)
GET https://openapi.doumaisupermarket.online/v1/devices

# 指定一台设备,按序列号(SN)
GET https://openapi.doumaisupermarket.online/v1/devices/{sn}

{sn} 换成设备的序列号,形如 GJ-354560230B34343043335435。它在设备标签上,App 的设备详情页也能看到。

只看得到你自己的设备

接口返回的永远是你账号名下的设备。传一个不属于你的 SN, 会得到 404 设备不存在 —— 和「这个 SN 根本不存在」是同一个回答。 这是刻意的:不告诉你「这台是别人的」,避免被用来探测别人的设备。

04 04 — 认证

认证Authentication

每次请求带上两个 HTTP 头,值就是上一节拿到的那一对:

Header填什么
X-Access-Key-IdAccessKey ID,如 gj_ak_3f9a2b7c1d8e4a5b
X-Access-Key-SecretAccessKey Secret,43 个字符
顺序与名字都不能错

两个头的名字很容易对调(一个叫 Id 一个叫 Secret),对调后一律 401。复制粘贴时前后不要带空格或换行 —— secret 里带个换行是这类问题最常见的原因。

一个完整的例子

curl
curl -s -i \
  -H "X-Access-Key-Id: gj_ak_3f9a2b7c1d8e4a5b" \
  -H "X-Access-Key-Secret: kJ8xQw3fR7mN2pL5tZ9vB1cD4eF6gH8jK0lM2nO4pQw" \
  "https://openapi.doumaisupermarket.online/v1/devices?fields=temperature,smoke"

成功时返回 200

200 OK
{
  "code": 200,
  "msg": "success",
  "data": {
    "list": [
      {
        "sn": "GJ-354560230B34343043335435",
        "name": "一号库房",
        "status": "online",
        "lastSeen": "2026-09-22T10:31:02",
        "temperature": 24.5,
        "smoke": 0
      }
    ],
    "total": 1
  }
}
body 里的 code 与 HTTP 状态码一致

这是开放 API 独有的约定(平台内部接口是另一套)。第三方按 HTTP 状态码body 的 code 写错误分支都行,两者永远相等。

05 05 — 字段

字段说明The fields

字段类型含义
snstring设备序列号。恒返回
namestring设备名称(你在 App 里起的)。恒返回
statusstring在线状态,三种取值见下表。恒返回
lastSeenstring最后一次收到数据的时间。恒返回,可能是 null
temperaturenumber温度,摄氏度。可选
smokeinteger烟雾,0 = 无 / 1 = 有。可选
waterinteger水浸,0 = 无 / 1 = 有。可选

status 的三种取值

什么情况下出现
online最近 7 分钟内报过数据,且当前没有未恢复的告警
alert未恢复的告警(烟雾 / 水浸 / 温度超标)。告警恢复后自动变回 online
offline超过 7 分钟没收到数据。设备断电、断网、或模块掉线都会这样

这个状态与 App 里看到的完全同源 —— 不会出现「App 说离线、API 说在线」。

⚠️ 最要紧的一段:三个读数「没数据」时长得不一样

设备这一轮没上报某个读数时,接口的表现并不一致。 这是上游设备的缺省逻辑决定的,接消防联动前请务必看懂:

  • temperature 没数据 → 响应里没有这个键 (不是 null,是整个键不存在)。
  • smoke / water 没数据 → 键存在,值是 0
  • smoke / water 连键都没有 → 只说明 这台设备连一条数据行都没有,从未上报过任何数据。

所以对 smoke / water 而言, 「设备这次没说」和「设备说没有」在接口上无法区分 —— 两者都是 0

不要这样读:smoke = 0 就代表「设备明确报告过无烟」

不能这么读。设备这一轮压根没提烟雾,返回的也是 0。 把 0 当作「已确认无烟」,等于把「没在监控」误当成「监控中且安全」。

要判断设备是不是还在正常工作,看 statuslastSeen不要只看读数

也别拿 lastSeen 给某个读数背书

lastSeen 只能证明设备报过东西,不能证明它报过温度 —— 设备可以只报烟雾、不报温度,此时 lastSeen 很新鲜而 temperature 依然没有键。

06 06 — 投影

只要部分字段Sparse fieldsets

如果你只要温度,加上 ?fields= 就行 —— 少传点数据,也少占带宽:

fields 用法
# 只要温度和烟雾
/v1/devices?fields=temperature,smoke

# 只要指定设备的水浸
/v1/devices/GJ-354560230B34343043335435?fields=water

# 省略 fields = 全要(等价于 temperature,smoke,water)
/v1/devices
写法结果
省略 fields三项全返回
?fields=(空值)三项全返回
?fields=temperature,smoke只返回这两项
?fields=temperature,temperature去重,等同只写一次
?fields=humidity400,见下
写错的字段名会被明确拒绝,不会静默忽略

传了不在 temperature / smoke / water 里的值, 会返回 400 并告诉你哪个字段不认:

400 Bad Request
{
  "code": 400,
  "msg": "fields 不支持的字段:temperture(可用:temperature, smoke, water)",
  "data": null
}

这是刻意的:如果静默忽略,你把 temperature 拼成 temperture 会拿到一个「HTTP 200 但少了字段」的响应,看起来一切正常, 只能在业务出问题时才发现。

另外,sn / name / status / lastSeen 恒返回,不受 fields 影响 —— 想让第三方把 status 筛掉是做不到的, 那等于允许把离线设备几小时前的温度当实时值去联动消防。

07 07 — 频率

调用频率Rate limit

⚠️ 最小调用间隔 30 秒,按「一把 key」计时

同一个 AccessKey 在 30 秒内第二次调用会直接返回 429。 这个窗口是每把 key 的,不是每台设备的 —— 你名下有 5 台柜子,也不代表可以 5 台各调一次。

正确与错误的做法

✅ 5 台柜子

一次 GET /v1/devices 拿回全部 5 台,每 30 秒一次。 一次请求就把所有设备都给了,不需要按 SN 遍历。

❌ 5 台柜子

循环调 5 次 /v1/devices/{sn} —— 第 2 次就是 429, 而且你要退避到 30 秒后才能继续,反而比调一次更慢。

只有在你确实只关心一台设备时,才用按 SN 的接口。 设备不止一台就一律用全量接口。

被拒了怎么办

429 的响应里带一个标准的 Retry-After 头,告诉你还要等几秒:

429 Too Many Requests
HTTP/1.1 429
Retry-After: 12

{
  "code": 429,
  "msg": "请求过于频繁,请在 12 秒后重试",
  "data": null
}

照着 Retry-After 退避即可,不必去解析中文提示。 建议你的定时任务设成 30 秒或更长(比如 60 秒), 把网速抖动、时钟偏差都躲开。

窗口边界是闭区间

距上次调用恰好 30.000 秒算通过,早 1 微秒算超频。 如果你设的定时器正好是 30 秒,会因为毫秒级的抖动偶尔吃一次 429 —— 留一点余量(30 秒以上)就没这个问题。

认证失败不占配额

只有认证通过的调用才计入 30 秒窗口。也就是说,凭据填错导致的一串 401 不会把你锁在 429 里 —— 但反过来,401 也不是因为超频造成的, 别把两种情况混在一起查。

08 08 — 错误码

错误码Error codes

HTTP意思你该怎么做
401 凭据无效或缺失(AccessKey 停用以外的所有认证失败) 检查两个头的名字有没有对调、值有没有带空格/换行;确认 key 没被删
403 AccessKey 已被停用 去 App 里重新启用这把 key。不用换 secret
429 调用过于频繁(距上次不足 30 秒) 按响应里的 Retry-After 秒数退避,然后把定时间隔调大
404 设备不存在,或不属于你 核对 SN 是否抄错;确认这台设备绑在你这个账号下
400 fields 里有不支持的字段名 msg 里列出的可用字段改正拼写
401 与 403 的区别很实用

401 = 凭据本身不对(要查配置); 403 = 凭据是对的,但被停了(去后台点一下就好,不用改配置)。 把这两个分开接,能省掉很多「到底该改哪边」的来回。

09 09 — 示例

代码示例Sample code

三份可直接改一改就用的实现。重点看错误处理 —— 尤其 429 的退避,那是长期运行必然会遇到的。

curl

shell
# -i 显示响应头,方便看 Retry-After
curl -s -i \
  -H "X-Access-Key-Id: $GJ_KEY_ID" \
  -H "X-Access-Key-Secret: $GJ_KEY_SECRET" \
  "https://openapi.doumaisupermarket.online/v1/devices?fields=temperature,smoke,water"

# 凭据放环境变量,别写进脚本、别提交进 git
# export GJ_KEY_ID="gj_ak_..."
# export GJ_KEY_SECRET="..."

Python(requests)

python
import os, time, requests

BASE = "https://openapi.doumaisupermarket.online/v1"
HEADERS = {
    "X-Access-Key-Id": os.environ["GJ_KEY_ID"],
    "X-Access-Key-Secret": os.environ["GJ_KEY_SECRET"],
}

def fetch_devices(fields="temperature,smoke,water"):
    """拉全部设备。返回 list;不可恢复的错误抛异常。"""
    r = requests.get(f"{BASE}/devices",
                     headers=HEADERS,
                     params={"fields": fields},
                     timeout=10)

    if r.status_code == 429:
        # 退避的标准做法:听服务端的 Retry-After
        wait = int(r.headers.get("Retry-After", 30))
        raise RateLimited(wait)

    if r.status_code in (401, 403):
        # 401 查配置;403 去 App 里启用
        raise AuthProblem(r.status_code, r.json().get("msg"))

    r.raise_for_status()
    return r.json()["data"]["list"]

# 主循环:30 秒以上一次,一次拿全部设备
while True:
    try:
        for d in fetch_devices():
            # ⚠️ smoke 为 0 不等于「确认无烟」,判在线看 status/lastSeen
            print(d["sn"], d["status"], d.get("temperature"))
        time.sleep(30)
    except RateLimited as e:
        time.sleep(e.wait)      # 按服务端说的秒数退避
    except requests.RequestException:
        time.sleep(10)          # 网络抖动,稍后重试

Java(java.net.http,Java 11+)

java
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

public class OpenApiClient {

    private static final String BASE =
            "https://openapi.doumaisupermarket.online/v1";

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private final String keyId, keySecret;

    public OpenApiClient(String keyId, String keySecret) {
        this.keyId = keyId;
        this.keySecret = keySecret;
    }

    /** 拉全部设备。429 会按 Retry-After 退避一次后重试。 */
    public String fetchDevices(String fields) throws Exception {
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(BASE + "/devices?fields=" + fields))
                .header("X-Access-Key-Id", keyId)
                .header("X-Access-Key-Secret", keySecret)
                .timeout(Duration.ofSeconds(10))
                .GET()
                .build();

        HttpResponse<String> resp =
                http.send(req, HttpResponse.BodyHandlers.ofString());

        if (resp.statusCode() == 429) {
            // 听服务端给的秒数,别自己猜
            String after = resp.headers().firstValue("Retry-After").orElse("30");
            Thread.sleep(Long.parseLong(after) * 1000L);
            resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        }

        if (resp.statusCode() != 200) {
            throw new IllegalStateException(
                    "开放 API 返回 " + resp.statusCode() + ": " + resp.body());
        }
        return resp.body();
    }
}
把凭据放进环境变量或密钥管理

三份示例都用环境变量。不要把 secret 硬编码进源码、 提交进 git、或写进前端页面 —— 它等同于你账号下全部设备数据的读权限。

10 10 — 排错

没调通?按这个顺序查Troubleshooting

返回 401,但 key 看着没错

按这个顺序排:

  1. 两个头的名字是不是对调了 —— Id 头要填 ID,Secret 头要填 secret。这是最常见的一个。
  2. 值有没有带空格或换行 —— 从 App 复制时很容易把行尾换行一起带上。把值打出来看长度:ID 是 gj_ak_ 加 16 个字符,secret 是 43 个字符。
  3. 这把 key 被删了吗 —— 删除即吊销,用它的调用立刻 401。去 App 列表里确认还在。
  4. 如果返回的是 403 而不是 401,见下一条。
返回 403 已停用

这把 key 是被停用了(不是删除)。去 App 里把它重新启用即可, secret 不用换、你的配置也不用改 —— 这就是「停用」和「删除」的区别。

一直 429

先分清是「真调太快」还是「有别人在调」:

  • 检查你的定时器间隔 —— 必须 ≥ 30 秒,且是按 key 算的。5 台设备循环调 5 次是最常见的成因。
  • 改成一个请求拿全部:GET /v1/devices
  • 如果确定自己 30 秒才调一次却还 429 —— 那说明同一把 key 还被别的程序在用(比如另一个人也配了同一把)。给每个系统单独建一把 key,既能避免互相挤,也能单独排查和吊销。
temperature 是 null,但设备明明在线

很可能这台设备从来没有上报过温度,或这一轮没报。 注意「没数据」的两种长相(见第五节):

  • 压根没有 temperature 这个键 → 设备从未报过温度,或本轮没报。
  • 如果你没写 fields 却看不到这个键,就是前者。

判断设备是否正常工作要看 statuslastSeen, 不要用某个读数是否存在来推断设备死没死。

status 是 offline,但设备明明通着电

offline 的判据是「超过 7 分钟没收到数据」, 不是「设备没电」。所以通电但报不上来,一样是 offline。按这个顺序查:

  1. 现场网络 —— 机柜处的路由器/交换机/4G 是否正常。
  2. 设备上的通信模块是否掉线(模块本身可能死机,断电重启可恢复)。
  3. lastSeen 停在了什么时候,能大致定位是哪一刻断的。

这条和 App 里显示的状态是同一个判定,不会两边不一致。

返回 404 设备不存在

两种可能,接口刻意不区分:

  • SN 抄错了 —— 对着设备标签或 App 详情页核对一遍(注意大小写和中间的连字符)。
  • 这台设备不属于你这个账号。开放 API 只返回你名下的设备。
返回 400 fields 不支持的字段

只有 temperaturesmokewater 三个可选字段, 拼写必须完全一致。msg 里会直接列出可用字段,照着改即可。

想确认「到底调没调通」

列表接口里有 lastRequestAt(上一次认证通过的调用时刻), null 表示从未成功调用过。App 的 AccessKey 列表里能看到它 —— 客户说「我调了但没数据」时,先看这个字段比看日志快。

注意它只记录认证通过的调用:一串 401 不会更新它。 所以「lastRequestAt 是 null」通常意味着凭据那一关就没过。