目录
- [东门 NVR、仓库 4G、周界枪机,为什么对不上账](#东门 NVR、仓库 4G、周界枪机,为什么对不上账 "#%E4%B8%9C%E9%97%A8-nvr%E4%BB%93%E5%BA%93-4g%E5%91%A8%E7%95%8C%E6%9E%AA%E6%9C%BA%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AF%B9%E4%B8%8D%E4%B8%8A%E8%B4%A6")
- 这篇只会碰到这几项能力
- [轮询、官方 App、一上来写原生,园区为什么扛不住](#轮询、官方 App、一上来写原生,园区为什么扛不住 "#%E8%BD%AE%E8%AF%A2%E5%AE%98%E6%96%B9-app%E4%B8%80%E4%B8%8A%E6%9D%A5%E5%86%99%E5%8E%9F%E7%94%9F%E5%9B%AD%E5%8C%BA%E4%B8%BA%E4%BB%80%E4%B9%88%E6%89%9B%E4%B8%8D%E4%BD%8F")
- 动手:从创建应用到值班台账数清
- 台账对上之后,再开远程看和回放
- 联调会踩的坑
- 真正省下的不是再买一台相机
东门 NVR、仓库 4G、周界枪机,为什么对不上账
以前园区安防的做法很朴素:东门一套 NVR,仓库几台 4G 机挂在保安私人 App,周界枪机再进另一套厂商软件。值班室三块屏,早会用 Excel 对数。点位少、人盯得住的时候,这套还能交差。
点位涨到几百路就废了。东门通道在 NVR 里是 12,仓库机还在私人号下,周界掉线要第二天对表才知道。再买十台相机,账还是三本。
缺的不是再挂一路镜头,而是用现行 OpenAPI 把设备 bindDevice 进同一个开发者资产,再用 listDeviceDetailsByPage 按页拉齐,摊进你自己的园区值班台账。
读完按这个顺序交差:
- 在开放平台创建应用,拿到
appId/appSecret。 - 签名自测通过,能拿到
accessToken。 - 抽几台核绑定,再
bindDevice,checkDeviceBindOrNot读回isMine=true。 pageSize=50翻页,扫完账号下全部设备。- 值班台账按通道计路,东门 / 仓库 / 周界能数清。
- 抽一台拔网,台账里对应点位变成
offline,在线数对得上。
工业园区几百路分散在 NVR、4G、私人 App。本文用现行 OpenAPI 的
bindDevice统一入账、listDeviceDetailsByPage翻页对账,带可运行 Node.js,验到本地/inventory数清路数为止。
这篇会用到这几项能力
bindDevice:把设备绑到开发者账号。code按机型传底部安全码或改过的设备密码;没安全码且未改密,文档允许传空。checkDeviceBindOrNot:先看isBind/isMine。别人账上的机,绑不进来。listDeviceDetailsByPage:分页拉设备详情。pageSize范围 1~50,page从 1 起。几百路就是翻几页,不是一次拉全量。- 返回里的
deviceStatus/channelStatus:online、offline、sleep、upgrading。sleep是休眠,不要按掉线叫人。 catalog能区分NVR和IPC。几百路不等于几百台:一台 NVR 下面有多条channelList。accessToken有效期 3 天,报TK1002再刷,不要每个请求都申。
你负责把点位绑进开发者资产、写清东门仓库周界,开放平台负责按页吐设备详情和在离线。
text
枪机 / 4G / NVR 通道
│
▼
bindDevice 入同一开发者账号
│
▼
listDeviceDetailsByPage(每页最多 50)
│
▼
你的值班台账(按 channelId 计路)
│
▼
东门 / 仓库 / 周界 在线数
轮询、官方 App、一上来写原生,园区为什么扛不住
轮询适合对台账、对在线,前提是设备已经在同一个开发者账号下。三套厂商各轮各的,不是统一接入,只是三份定时任务。
只靠官方 App,红点停在保安私人手机里。交接班对不上序列号,仓库机跟着人走,园区后台永远是「少几路」。
一上来做原生客户端更没必要。园区已经有 OA 和值班大屏,缺的是 HTTP 把资产拉进自己的库,不是第三个看监控的 App。
动手:从创建应用到值班台账数清
部署只是开胃菜。真正过关的是:账号下的机全部出现在 /inventory,路数对、区域对,抽一台掉线数字会变。
第一步:创建应用,先填园区点位表
打开 乐橙开放平台 注册并创建应用。控制台「我的应用 - 应用信息」能看到 appId / appSecret 再往下做。没有这两样,后面签名都会废。
先不要写接口。用一张表把现场点位收拢:序列号、区域、通道、验证码来源。NVR 只记一台设备,通道另行列。设备先配网上线,能在官方 App 预览,再谈绑定。
text
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
PORT=8080
# 园区点位(deviceId 或 deviceId:channelId)
ZONE_EAST_GATE=东门NVR序列号
ZONE_WAREHOUSE=仓库4G序列号
ZONE_PERIMETER=周界枪机序列号
成功:控制台能看到应用;点位表每行都有序列号和区域,没有「在某某私人 App 里」。
兜底:还停在私人号下的机,先从点位表标出来,第三步再核绑定。控制台都没有应用,不要往下抄代码。
第二步:签名壳,先和文档案例对齐
请求走 https://openapi.lechange.cn/openapi/{method},壳子是 system + params + id。签名按开发规范:time:{time},nonce:{nonce},appSecret:{appSecret} 做 MD5 小写 32 位。time 和服务器误差不能超过 5 分钟,否则 SN1002;nonce 5 分钟内不能重复,否则 SN1005。
javascript
// openapi-client.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';
function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
async function callOpenApi(method, appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = uuidv4();
const body = {
system: { ver: '1.0', appId, time, nonce, sign: calcSign(time, nonce, appSecret) },
id: uuidv4(),
params,
};
const res = await fetch(`${OPENAPI_BASE}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (!json.result || json.result.code !== '0') {
const msg = json.result ? `${json.result.code} ${json.result.msg}` : JSON.stringify(json);
throw new Error(`OpenAPI ${method} failed: ${msg}`);
}
return json.result.data;
}
module.exports = { callOpenApi, calcSign };
成功:calcSign(1706511734, 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2', 'test123456789test123456789') 得到 fd37b62889e4757c58b8f3bf05fb9976;再调 accessToken 能打印 token 和 expireTime。
兜底:对不上标准案例,先别查业务接口,多半是字符串拼错。TK1002 再刷 token。签名算法若控制台提示不一致,以现行开发规范为准,不要混网上过时示例。
第三步:先核绑定,再一台一台 bindDevice
这篇入账接口是 bindDevice。先 checkDeviceBindOrNot:isBind=false 再绑;isMine=true 已经是你的;isBind=true 且 isMine=false 在别人账上,先解绑或走交接,不要死磕。
text
token=上一步拿到的 accessToken
deviceId=仓库4G序列号
code=底部8位安全码或改过的设备密码
未改密且标签没有 8 位安全码,按文档把 code 传空。一批机不要写循环闷头绑,先抽东门、仓库、周界各一台。
应用开发写明:部分新出厂或升级后的设备,可能无法只靠 HTTP bindDevice 完成绑定,要结合客户端 SDK。联调绑失败、核绑定又显示未在本账号,先走这条说明,不要改签名。
成功:bindDevice 返回 code=0;再查一次,isBind=true 且 isMine=true。
兜底:已经是别人的资产,先让现场从原账号解绑。新机 HTTP 绑不上,按应用开发说明改 SDK 路径。
第四步:pageSize 写成 50,把账号翻完
这篇对账接口是 listDeviceDetailsByPage。pageSize 最大 50,自有资产用 source=bind;连分享进来的也要入账,用默认的 bindAndShare。翻页直到某一页 deviceList 为空,或累计条数不再增加。
javascript
// sync-devices.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');
async function listAllDevices(appId, appSecret, token) {
const all = [];
let page = 1;
const pageSize = 50;
while (true) {
const data = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
token,
page,
pageSize,
source: 'bind',
});
const chunk = data.deviceList || [];
all.push(...chunk);
if (chunk.length < pageSize) break;
page += 1;
}
return all;
}
function flattenChannels(deviceList, zoneOf) {
const rows = [];
for (const d of deviceList) {
const channels = d.channelList && d.channelList.length ? d.channelList : [{ channelId: 0, channelStatus: d.deviceStatus }];
for (const ch of channels) {
rows.push({
deviceId: d.deviceId,
deviceName: d.deviceName,
catalog: d.catalog,
deviceStatus: d.deviceStatus,
channelId: ch.channelId,
channelStatus: ch.channelStatus || d.deviceStatus,
deviceVersion: d.deviceVersion,
zone: zoneOf(d.deviceId),
});
}
}
return rows;
}
module.exports = { listAllDevices, flattenChannels };
成功:控制台打印出 deviceId、catalog、deviceStatus、deviceVersion;有 NVR 时,同一 deviceId 下出现多条 channelId。400 路大约 8 页,几百路听起来吓人,其实就是 pageSize=50 翻几页。把每次返回的 count 和本页 deviceList.length 记下来,方便对「少了一页」这种问题。
兜底:只打了第 1 页就停,台账会永远少一大截。pageSize 写成 100,以现行文档为准,合法值是 1~50。分享进来的机要用 bindAndShare,只写 bind 会漏。
第五步:值班台账服务,按通道计路
本地映射区域,摊平通道,露出 /inventory。这是接收侧:开放平台吐列表,你的服务收成园区能看的一张账。还没数清之前,不要去创建直播。
text
# zones.json 填写模板(对齐写字段)
{
"东门NVR序列号": "东门",
"仓库4G序列号": "仓库",
"周界枪机序列号": "周界"
}
javascript
// inventory-server.js
require('dotenv').config();
const express = require('express');
const { callOpenApi } = require('./openapi-client');
const { listAllDevices, flattenChannels } = require('./sync-devices');
const ZONES = require('./zones.json');
function zoneOf(deviceId) {
return ZONES[deviceId] || '未分区';
}
let cache = { rows: [], syncedAt: 0 };
async function refresh() {
const token = (
await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})
).accessToken;
const devices = await listAllDevices(process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, token);
cache = { rows: flattenChannels(devices, zoneOf), syncedAt: Date.now() };
}
const app = express();
app.get('/health', (_req, res) => res.status(200).send('ok'));
app.get('/inventory', async (_req, res) => {
if (!cache.rows.length) await refresh();
const online = cache.rows.filter((r) => r.channelStatus === 'online').length;
const offline = cache.rows.filter((r) => r.channelStatus === 'offline').length;
const sleep = cache.rows.filter((r) => r.channelStatus === 'sleep').length;
res.json({
totalChannels: cache.rows.length,
online,
offline,
sleep,
syncedAt: cache.syncedAt,
rows: cache.rows,
});
});
app.post('/inventory/refresh', async (_req, res) => {
await refresh();
res.json({ totalChannels: cache.rows.length, syncedAt: cache.syncedAt });
});
app.listen(process.env.PORT || 8080);
成功:浏览器打开 /health 返回 ok;GET /inventory 能看到 totalChannels、按区域分好的 rows,以及 deviceVersion。值班大屏只读这个 JSON,不要让前端直接拿 appSecret 去翻页。
兜底:数字对不上现场,先看是不是按设备计台、没按通道计路。未分区 太多,回去补 zones.json,不要先改签名。refresh 里已经申过 token,同一次同步不要再打 accessToken。
第六步:抽一台拔网,台账变了才算闭环
按勾,不要跳步。
/inventory的totalChannels对得上点位表(NVR 按通道加总)。- 东门、仓库、周界三行都能对上
deviceId。 - 抽仓库或周界一台拔网 / 断 4G,调
POST /inventory/refresh,该行channelStatus或deviceStatus变为offline,offline计数加一。 - 插回网线,再刷一次,回到
online。
成功:上面四条都对得上。这就是「值班台账数清了」。
兜底:列表有机、刷新状态不变,隔一两分钟再刷;4G 上的 sleep 不要按掉线打电话。单台复核可用 listDeviceDetailsByIds 或 deviceOnline。注意:deviceOnline 的 onLine 是 0/1/3/4,和列表里的英文枚举不是同一套字,对账时不要混着比。
台账对上之后,再开远程看和回放
最小闭环已经成立,才加第二层。仍用同一套现行 OpenAPI,不要另起一套协议。
值班员要点开东门看一眼:先 bindDeviceLive 创建设备源直播,再 getLiveStreamInfo 取 HLS。没创建过直播,后一个接口拿不到地址。直播地址对外等于公开画面,只给值班内网,不要进公共群。
事后取证:同一天时段用 queryCloudRecords 查云录像片段(queryRange 最大 100),再用 createDeviceRecordHls 生成回放地址。beginTime / endTime 格式是 yyyy-MM-dd HH:mm:ss,文档写明不支持跨天。recordType 取 cloudRecord 或 localRecord,以该点位是否开通云存储、是否有本地卡为准。
告警进工单、遮挡掉线进群,是下一篇的事。这篇停在「一张账能数清,抽一台掉线数字会变」。
联调会踩的坑
| 现象 | 多半是什么 | 先做什么 |
|---|---|---|
| 台账只有几十路 | 只拉了第 1 页 | pageSize=50 一直翻到不足一页 |
| 现场 200 路,台账 40 台 | 按设备计台,没按通道计路 | 展开 channelList,NVR 按 channelId 加 |
| 仓库机列表里没有 | 还在私人 App,isMine=false |
checkDeviceBindOrNot,再 bindDevice |
| HTTP 绑失败 | 新机 / 新固件不走纯 HTTP 绑定 | 看应用开发的 SDK 说明 |
把 sleep 当成掉线打电话 |
4G 省电 | 和 offline 分开看 |
| 签名对不上标准案例 | 原始串拼错或混了过时算法 | 先对齐 MD5 案例,再谈业务 |
| 隔天一批接口全挂 | token 过了约 3 天 | 收到 TK1002 再刷,不要每个请求都申 |
| 列表英文状态和单查对不上 | deviceOnline 用的是 0/1/3/4 |
两套枚举分开映射 |
真正省下的不是再买一台相机
这篇省下的不是多一路镜头,而是值班室不用三块屏、三份 Excel,才能回答「园区现在在线多少路、掉的是东门还是仓库」。
只做网页预览、不建资产台账的,去看轻应用嵌入那条线就够了,不必在这里把翻页和区域映射搭起来。家里两台相机、出事了有人盯 App 的散户,也没必要上这一套。
appSecret、设备密码只放环境变量。直播地址当密钥管。日志里的序列号对外部打码。接口以现行文档为准,不要混已标注不再维护的栏目。