先搞清楚它是什么What it is
你可以把回调理解成「机器的短信」:短信发给人的手机,回调发给你的系统。 两者同时存在、互不影响 —— 收到短信的人负责去现场处理,收到回调的系统负责记录、派单、上大屏。
| 对比项 | 短信 / 语音 | 回调(Webhook) |
|---|---|---|
| 送达对象 | 你的手机 | 你自己的服务器网址 |
| 内容 | 一句话提醒 | 结构化 JSON,含设备序列号、告警类型、读数、时间 |
| 费用 | 消耗余额(按条 / 按分钟) | 免费 |
| 余额为 0 时 | 停发 | 照常发送 |
| 失败处理 | 不重试 | 按 1 / 5 / 30 分钟自动补发 |
| 需要你做什么 | 无,手机号即账号 | 提供一个能收 POST 的网址 |
只有「新告警」才会回调。同一台机柜的同一种告警,在恢复之前只推一次 ——
不会因为设备每分钟上报一次就刷你一次。
告警恢复(恢复正常)时不会回调,与短信、语音的行为一致。
建议两者都留着。回调是发给机器的,机器挂了、网断了、程序崩了,没人会知道; 短信是发给人的,人总能看见。真正的消防场景里,这两条链路应该是冗余而不是二选一。
在 App 里填地址Set it up
整个配置只有一件事:把「告警回调」的开关打开,填上你的网址,保存。 地址是每个账号一个,账号下所有机柜共用同一个地址。
-
进入通知设置
打开 App → 我的 → 通知设置,找到「告警回调」这一项。
-
填写回调地址
填写一个以
https://或http://开头的完整网址,例如https://erp.yourcompany.com/ganjiang/hook。对这个地址的四个要求- 能收 POST 请求 —— 它不是个网页,随便打开返回什么都行;重点是能处理 POST。
- 公网可达 —— 我们的服务器要能从外面连上它。只在公司内网能打开的地址不行。
- 不能是内网或本机地址 ——
localhost、127.0.0.1、192.168.x.x、10.x.x.x这类地址会被当场拒绝, 并明确提示原因。(这是为了防止误填后我们的服务器每次告警都去请求它自己。) - 长度不超过 512 个字符。
-
保存
保存成功即生效,不需要重启设备,也不需要等审核。之后第一台设备报警时就会推过去。
-
确认对方真的收到了
最稳的验证方式:让运维在机柜上做一次真实告警测试(或触发一次烟雾测试), 然后看你的服务器有没有收到请求。配置完一定要验一次 —— 没有验过的回调地址,等于没有配。
填写前先自检一下地址
此工具只做本地初步检查,最终以 App 保存时的服务端校验为准。
公网可达性无法在这里验证 —— 那要等真实告警时才知道。
保存失败时的提示含义
| App 上的提示 | 说明 |
|---|---|
| 回调地址必须以 http:// 或 https:// 开头 | 漏了协议头,或写成了别的协议 |
| 回调地址缺少主机名 | 形如 https:///hook,只有斜杠没有域名 |
| 回调地址格式不正确 | 含有空格等非法字符,或不是合法网址 |
| 回调地址不能指向内网或本机地址 | 填了 localhost / 127.0.0.1 / 192.168.x.x 等 |
| 回调地址过长,上限 512 字符 | 地址太长,通常是带了很长的参数串 |
如果这次保存被拒绝,之前已经填好的地址会原样保留, 也不会出现「开关存上了、地址没存上」的半截状态。
你会收到什么The payload
一个标准的 HTTP POST 请求,请求体是 UTF-8 编码的 JSON,
Content-Type: application/json; charset=utf-8。
没有签名、没有自定义请求头,对方服务器拿到请求体直接当 JSON 解析即可。
{
"event": "alert.created",
"alertLogId": 1234,
"deviceSn": "GJ20240001",
"deviceName": "A区1号柜",
"alertType": "smoke",
"alertTypeCn": "烟雾",
"alertValue": "1",
"occurredAt": "2026-09-22T10:30:00.123"
}
| 字段 | 类型 | 说明 |
|---|---|---|
| event | string | 事件名称,目前恒为 alert.created。请用它做分支判断,将来可能增加新的事件类型。 |
| alertLogId | number | 这条告警的唯一编号。幂等键 —— 同一个编号可能被推两次(见重试说明),请按它去重。 |
| deviceSn | string | 设备序列号。用它来定位是哪台机柜 —— 这是稳定的、不会变的。 |
| deviceName | string | 设备名称(如「A区1号柜」)。用户随时可以改,仅供展示,不要当标识用。 |
| alertType | string | 告警类型原始值:smoke / water / temperature。程序分支用这个。 |
| alertTypeCn | string | 同上,中文:烟雾 / 水浸 / 温度过高。给展示和推送用,省得你自己维护一张映射表。 |
| alertValue | string | 告警读数 / 取值,可能为 null,使用时请判空。 |
| occurredAt | string |
告警发生时间,ISO 格式,服务器时区(UTC+8),毫秒精度。
注意不带时区后缀(不是 Z 结尾),请按北京时间解析。
|
怎么对接For developers
这一节给负责写接收端的人。四条规则,缺一条都会踩坑。
只要返回 HTTP 2xx(200、201、202、204 都行)我们就认为成功。 返回 4xx、5xx、超时或连不上,都会判定为失败并进入重试。 响应的内容是什么无所谓,我们不看。
超时限制:建连 3 秒 / 读取 5 秒。所以正确的做法是 先立刻返回 200,再把活丢进队列慢慢干。 如果你在请求处理里同步查数据库、发钉钉、写工单,一旦超过 5 秒就会超时 —— 我们会认为失败了,然后又给你推一遍。
你一定会收到重复的告警。 比如对方已经收到但响应慢了 5 秒,
我们判定超时 → 1 分钟后再推一次。这是重试机制的必然结果,不是故障。
请用 alertLogId 做主键或唯一索引做幂等,
否则同一场火会被派两次工单。
如果在这期间用户在 App 里关掉了回调、开启了静音或清空了地址, 后续补发会立即取消。所以「收到一条重试的告警」说明用户此刻仍希望收到它。
最小可用实现
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
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"); // 先应答,别等业务处理完
});
把自己的地址填进去,确认对方服务确实能接收并返回 2xx:
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"}'
安全须知Security
我们没有对请求做签名或加密校验。这意味着 任何知道这个网址的人,都能伪造一条告警推给你的服务器。 请把它当作密码来对待。
- 务必使用 HTTPS 地址。 明文 http 会让告警内容在链路上被人看到。
- 网址里带一段随机字符,例如
https://erp.yourcompany.com/hook/gj-7f3a91c4。 这比/hook这种可猜的路径安全得多 —— 路径本身就是那道防线。 - 不要把这个网址发到群里、工单里或公开文档里。
- 你的服务器应该只接受来自我们服务器的请求,
并在业务层再做一次校验(比如只处理已知的
deviceSn)。 - 地址一旦外泄,直接在 App 里改成一个新地址即可 —— 旧地址立刻失效,不需要联系客服。
没收到?按这个顺序查Troubleshooting
一条回调都没收到过
- 地址填了吗? 没填地址时,「告警回调」在 App 里是置灰不可用的状态,告警来了也不会推。
- 通知总开关 / 静音状态? 关掉总开关、或处于静音期间,回调会和短信一起停发。
- 回调开关是开的吗? 渠道开关关掉后同样不发。
- 地址公网能打开吗? 用手机 4G(不要连公司 WiFi)访问一下,确认不是只在内网可达。
- 有真的发生过告警吗? 只有新告警才会推。设备一直正常,自然不会有回调。
收到过,但后来就没了
先看是不是被频控了。 同一个账号在 60 秒(默认值)内只会收到一条回调 —— 这是为了防设备抖动把你的服务器刷爆。间隔可以在 App 里调(30 秒 ~ 1 小时)。
另外同一台机柜的同一种告警在恢复前只推一次。如果柜子一直在报警但一直没有好转, 你只会收到第一条。
收到重复的告警
这是重试机制的正常表现。对方服务器如果没在 5 秒内返回 2xx(或者返回了非 2xx、 连接超时),我们就会按 1 / 5 / 30 分钟再推一次,直到成功或用完 4 次机会。
处理办法是按 alertLogId 去重。如果你的接收端处理比较慢,
请改成本地先落库、立刻返回 200,再异步处理。
App 提示「回调地址不能指向内网或本机地址」
你填的是 localhost、127.0.0.1、192.168.x.x、
10.x.x.x、172.16~31.x.x、169.254.x.x
这类内部地址。因为我们的服务器在公网上,连不到你的内网;
而且更糟的是,如果不去拦,就相当于让我们的服务器去请求它自己。
请填写一个公网可达的地址(域名或公网 IP 都行)。
余额用完了,还能收到回调吗?
能。 回调不产生电信费用,所以它不受余额限制。 余额耗尽时短信和语音会停发,回调照常工作。
静音了,还会回调吗?
不会。 静音和总开关对三个渠道一视同仁。 用户点「静音 2 小时」的意思是「这两个小时别打扰我」—— 只静音手机而照旧往服务器推送,不符合这个意图。
想临时停掉回调
三种方式,按影响范围从小到大:
- 关掉「告警回调」开关 —— 只停回调,短信语音照常。
- 清空回调地址 —— 效果同上。
- 临时静音 —— 三个渠道全停,到点自动恢复。
联系客服时请提供:设备序列号、告警大致时间、你填的回调地址。
我们能在服务端日志里查到每一次投递的尝试次数、失败原因(如 http_500、
timeout、connect_failed),定位是发不出去还是对方没收下。