干将智联 / 告警回调接入说明 / V1.0 · 2026-09-22 ← 返回首页

告警通知能力 · Webhook

把告警推到你自己的服务器

机柜一旦冒烟、进水或温度超标,除了给你手机发短信,干将服务器还能把这条告警 实时 POST 到你自己填写的网址。这样告警就能直接落进你的监控大屏、ERP、 工单系统或值班群里 —— 不需要人工转发,也不额外收费

信号流向 · SIGNAL PATH DEVICE → YOUR SERVER
01 · Source 机柜设备 烟雾 / 水浸 / 温度
02 · Relay 干将服务器 判定 · 去重 · 重试
03 · Target 你的服务器 返回 2xx 即送达

没有返回 2xx → 自动补发,最多 4 次尝试

第 1 次立即 隔 1 分钟第 2 次 隔 5 分钟第 3 次 隔 30 分钟第 4 次 放弃记入日志
01 01 — 概念

先搞清楚它是什么What it is

你可以把回调理解成「机器的短信」:短信发给人的手机,回调发给你的系统。 两者同时存在、互不影响 —— 收到短信的人负责去现场处理,收到回调的系统负责记录、派单、上大屏。

对比项短信 / 语音回调(Webhook)
送达对象你的手机你自己的服务器网址
内容一句话提醒结构化 JSON,含设备序列号、告警类型、读数、时间
费用消耗余额(按条 / 按分钟)免费
余额为 0 时停发照常发送
失败处理不重试按 1 / 5 / 30 分钟自动补发
需要你做什么无,手机号即账号提供一个能收 POST 的网址
什么时候会收到回调

只有「新告警」才会回调。同一台机柜的同一种告警,在恢复之前只推一次 —— 不会因为设备每分钟上报一次就刷你一次。
告警恢复(恢复正常)时不会回调,与短信、语音的行为一致。

回调 ≠ 替代短信

建议两者都留着。回调是发给机器的,机器挂了、网断了、程序崩了,没人会知道; 短信是发给人的,人总能看见。真正的消防场景里,这两条链路应该是冗余而不是二选一。

02 02 — 配置

在 App 里填地址Set it up

整个配置只有一件事:把「告警回调」的开关打开,填上你的网址,保存。 地址是每个账号一个,账号下所有机柜共用同一个地址。

  1. 进入通知设置

    打开 App → 我的 → 通知设置,找到「告警回调」这一项。

  2. 填写回调地址

    填写一个以 https://http:// 开头的完整网址,例如 https://erp.yourcompany.com/ganjiang/hook

    对这个地址的四个要求
    • 能收 POST 请求 —— 它不是个网页,随便打开返回什么都行;重点是能处理 POST。
    • 公网可达 —— 我们的服务器要能从外面连上它。只在公司内网能打开的地址不行。
    • 不能是内网或本机地址 —— localhost127.0.0.1192.168.x.x10.x.x.x 这类地址会被当场拒绝, 并明确提示原因。(这是为了防止误填后我们的服务器每次告警都去请求它自己。)
    • 长度不超过 512 个字符。
  3. 保存

    保存成功即生效,不需要重启设备,也不需要等审核。之后第一台设备报警时就会推过去。

  4. 确认对方真的收到了

    最稳的验证方式:让运维在机柜上做一次真实告警测试(或触发一次烟雾测试), 然后看你的服务器有没有收到请求。配置完一定要验一次 —— 没有验过的回调地址,等于没有配。

填写前先自检一下地址

此工具只做本地初步检查,最终以 App 保存时的服务端校验为准。
公网可达性无法在这里验证 —— 那要等真实告警时才知道。

保存失败时的提示含义

App 上的提示说明
回调地址必须以 http:// 或 https:// 开头漏了协议头,或写成了别的协议
回调地址缺少主机名形如 https:///hook,只有斜杠没有域名
回调地址格式不正确含有空格等非法字符,或不是合法网址
回调地址不能指向内网或本机地址填了 localhost / 127.0.0.1 / 192.168.x.x
回调地址过长,上限 512 字符地址太长,通常是带了很长的参数串
保存失败不会弄坏原有配置

如果这次保存被拒绝,之前已经填好的地址会原样保留, 也不会出现「开关存上了、地址没存上」的半截状态。

03 03 — 数据

你会收到什么The payload

一个标准的 HTTP POST 请求,请求体是 UTF-8 编码的 JSON, Content-Type: application/json; charset=utf-8没有签名、没有自定义请求头,对方服务器拿到请求体直接当 JSON 解析即可。

Request · 实际发出的请求体
{
  "event": "alert.created",
  "alertLogId": 1234,
  "deviceSn": "GJ20240001",
  "deviceName": "A区1号柜",
  "alertType": "smoke",
  "alertTypeCn": "烟雾",
  "alertValue": "1",
  "occurredAt": "2026-09-22T10:30:00.123"
}
字段类型说明
eventstring 事件名称,目前恒为 alert.created请用它做分支判断,将来可能增加新的事件类型。
alertLogIdnumber 这条告警的唯一编号。幂等键 —— 同一个编号可能被推两次(见重试说明),请按它去重。
deviceSnstring 设备序列号。用它来定位是哪台机柜 —— 这是稳定的、不会变的。
deviceNamestring 设备名称(如「A区1号柜」)。用户随时可以改,仅供展示,不要当标识用
alertTypestring 告警类型原始值:smoke / water / temperature程序分支用这个。
alertTypeCnstring 同上,中文:烟雾 / 水浸 / 温度过高。给展示和推送用,省得你自己维护一张映射表。
alertValuestring 告警读数 / 取值,可能为 null,使用时请判空。
occurredAtstring 告警发生时间,ISO 格式,服务器时区(UTC+8),毫秒精度。 注意不带时区后缀(不是 Z 结尾),请按北京时间解析。
04 04 — 对接

怎么对接For developers

这一节给负责写接收端的人。四条规则,缺一条都会踩坑。

规则 1 · 返回 2xx 就算送达

只要返回 HTTP 2xx(200、201、202、204 都行)我们就认为成功。 返回 4xx、5xx、超时或连不上,都会判定为失败并进入重试。 响应的内容是什么无所谓,我们不看。

规则 2 · 必须很快应答

超时限制:建连 3 秒 / 读取 5 秒。所以正确的做法是 先立刻返回 200,再把活丢进队列慢慢干。 如果你在请求处理里同步查数据库、发钉钉、写工单,一旦超过 5 秒就会超时 —— 我们会认为失败了,然后又给你推一遍。

规则 3 · 必须按 alertLogId 去重

你一定会收到重复的告警。 比如对方已经收到但响应慢了 5 秒, 我们判定超时 → 1 分钟后再推一次。这是重试机制的必然结果,不是故障。 请用 alertLogId 做主键或唯一索引做幂等, 否则同一场火会被派两次工单。

规则 4 · 重试会在用户改主意时停下

如果在这期间用户在 App 里关掉了回调、开启了静音或清空了地址, 后续补发会立即取消。所以「收到一条重试的告警」说明用户此刻仍希望收到它。

最小可用实现

Python · Flask
from flask import Flask, request, jsonify

app = Flask(__name__)
seen = set()      # 生产环境请换成数据库唯一索引

@app.post("/ganjiang/hook")
def hook():
    d = request.get_json(force=True)

    # ① 幂等:同一条告警会被重推,必须去重
    if d["alertLogId"] in seen:
        return "ok", 200
    seen.add(d["alertLogId"])

    # ② 真正的处理丢进队列,不要在这里做
    queue.put({
        "sn":   d["deviceSn"],      # 用 SN 定位机柜
        "type": d["alertType"],    # smoke / water / temperature
        "val":  d["alertValue"],  # 可能为 null
        "at":   d["occurredAt"],  # UTC+8,无时区后缀
    })

    # ③ 先回 2xx —— 超过 5 秒就算失败,会触发重复推送
    return "ok", 200
Node.js · Express
const seen = new Set();   // 生产环境换成唯一索引

app.post("/ganjiang/hook", (req, res) => {
  const d = req.body;

  if (seen.has(d.alertLogId)) return res.send("ok");  // 幂等
  seen.add(d.alertLogId);

  queue.push({
    sn:   d.deviceSn,      // 用 SN 定位机柜
    type: d.alertType,     // smoke / water / temperature
    at:   d.occurredAt,    // UTC+8,无时区后缀
  });

  res.send("ok");          // 先应答,别等业务处理完
});
用 curl 手工模拟一次

把自己的地址填进去,确认对方服务确实能接收并返回 2xx:

Shell
curl -i -X POST https://erp.yourcompany.com/ganjiang/hook \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{"event":"alert.created","alertLogId":1234,"deviceSn":"GJ20240001",
       "deviceName":"A区1号柜","alertType":"smoke","alertTypeCn":"烟雾",
       "alertValue":"1","occurredAt":"2026-09-22T10:30:00.123"}'
05 05 — 安全

安全须知Security

回调请求没有签名,地址本身就是凭证

我们没有对请求做签名或加密校验。这意味着 任何知道这个网址的人,都能伪造一条告警推给你的服务器。 请把它当作密码来对待。

  • 务必使用 HTTPS 地址。 明文 http 会让告警内容在链路上被人看到。
  • 网址里带一段随机字符,例如 https://erp.yourcompany.com/hook/gj-7f3a91c4。 这比 /hook 这种可猜的路径安全得多 —— 路径本身就是那道防线。
  • 不要把这个网址发到群里、工单里或公开文档里。
  • 你的服务器应该只接受来自我们服务器的请求, 并在业务层再做一次校验(比如只处理已知的 deviceSn)。
  • 地址一旦外泄,直接在 App 里改成一个新地址即可 —— 旧地址立刻失效,不需要联系客服。
06 06 — 排错

没收到?按这个顺序查Troubleshooting

一条回调都没收到过
  1. 地址填了吗? 没填地址时,「告警回调」在 App 里是置灰不可用的状态,告警来了也不会推。
  2. 通知总开关 / 静音状态? 关掉总开关、或处于静音期间,回调会和短信一起停发。
  3. 回调开关是开的吗? 渠道开关关掉后同样不发。
  4. 地址公网能打开吗? 用手机 4G(不要连公司 WiFi)访问一下,确认不是只在内网可达。
  5. 有真的发生过告警吗? 只有新告警才会推。设备一直正常,自然不会有回调。
收到过,但后来就没了

先看是不是被频控了。 同一个账号在 60 秒(默认值)内只会收到一条回调 —— 这是为了防设备抖动把你的服务器刷爆。间隔可以在 App 里调(30 秒 ~ 1 小时)。

另外同一台机柜的同一种告警在恢复前只推一次。如果柜子一直在报警但一直没有好转, 你只会收到第一条。

收到重复的告警

这是重试机制的正常表现。对方服务器如果没在 5 秒内返回 2xx(或者返回了非 2xx、 连接超时),我们就会按 1 / 5 / 30 分钟再推一次,直到成功或用完 4 次机会。

处理办法是alertLogId 去重。如果你的接收端处理比较慢, 请改成本地先落库、立刻返回 200,再异步处理。

App 提示「回调地址不能指向内网或本机地址」

你填的是 localhost127.0.0.1192.168.x.x10.x.x.x172.16~31.x.x169.254.x.x 这类内部地址。因为我们的服务器在公网上,连不到你的内网; 而且更糟的是,如果不去拦,就相当于让我们的服务器去请求它自己。

请填写一个公网可达的地址(域名或公网 IP 都行)。

余额用完了,还能收到回调吗?

能。 回调不产生电信费用,所以它不受余额限制。 余额耗尽时短信和语音会停发,回调照常工作。

静音了,还会回调吗?

不会。 静音和总开关对三个渠道一视同仁。 用户点「静音 2 小时」的意思是「这两个小时别打扰我」—— 只静音手机而照旧往服务器推送,不符合这个意图。

想临时停掉回调

三种方式,按影响范围从小到大:

  • 关掉「告警回调」开关 —— 只停回调,短信语音照常。
  • 清空回调地址 —— 效果同上。
  • 临时静音 —— 三个渠道全停,到点自动恢复。
还查不出来?

联系客服时请提供:设备序列号告警大致时间你填的回调地址。 我们能在服务端日志里查到每一次投递的尝试次数、失败原因(如 http_500timeoutconnect_failed),定位是发不出去还是对方没收下。