by tcdos 2026-07-08

运维人的瑞士军刀 - 设备管理、远程控制与监控

一、引言

充电宝机柜分布在城市的各个角落,运维人员不可能天天跑现场。所以后台必须提供一个「远程控制中心」——能看到每台设备的在线状态、信号强度、电池槽位详情,还能远程重启、强制弹宝、查询明细。

这一篇就讲设备管理模块的几个核心功能:设备列表与数据导入、远程指令操控、设备详情实时面板、心跳数据监控,以及 IoT 指令全量日志。


二、设备列表:在线 / 离线一目了然

设备列表是运维人员每天打开的第一个页面。核心要求就两条:

页面呈现:

设备表头包含设备编号、二维码编号、所在店铺、心跳时间、登录时间、型号、充电槽数量、可借/可还数量、信号强度、厂家、硬件版本、固件版本等信息。

在线状态用一个小圆点直观标识:

<el-badge :is-dot="scope.row.online === 1" :offset="[8, 8]" type="success">
    {{ scope.row.sn }}
</el-badge>

后端如何判断在线?

设备在线状态不是存在数据库里的,而是实时查 Socket 连接池:

public int? Connected(string sn)
{
    List<KeyValuePair<Socket, SocketClient>> clients =
        socketServer.OnlineSocketClients.ToList();
    foreach (var client in clients)
    {
        if (client.Value?.SN == sn)
            return 1;  // 在线
    }
    return 0;  // 离线
}

每次查询设备列表时,查出设备数据后再逐一检查在线状态:

var devices = mapper.Map<List<DeviceResDto>>(
    await deviceService.GetListAsync(strQuery));

foreach (var device in devices)
{
    device.Online = iotService.Connected(device.SN);
}

// 在线设备排前面
return devices.OrderByDescending(x => x.Online).ToList();

在线设备始终排在最前面,方便运维快速定位。

筛选与搜索:

支持按店铺筛选、按设备编号/二维码编号搜索,以及分页浏览。

批量导入:

几百台设备不可能一台台手动录入。后端封装了 NPOI 的 Excel 导入导出:

// 模板下载
[Route("down/template")]
[HttpGet]
public FileResult DownloadTemplate()
{
    var file = NPOIExcelHelper.Export(devices, sheetName);
    return File(file, "application/octet-stream",
        $"devices_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}

// 批量导入
[Route("import")]
[HttpPost]
public async Task<ResultModel> Import([FromForm] UploadFile upload)
{
    var table = NPOIExcelHelper.Import(excelFile);
    var devices = NPOIExtension.ConvertToEntity<ZnDeviceEntity>(table);
    await deviceService.AddRangeAsync(devices);
}

三、远程控制:三条指令搞定

设备详情页是运维人员的远程操作面板。页面顶部展示设备基本信息,中部是电池槽位列表,顶部三个操作按钮:

每个按钮对应一条 TCP 指令。

1. 重启设备

const cmdReboot = () => {
    loading.value = true;
    cmdSend({
        msg: {
            aims: device.value.aims,
            sn: device.value.sn,
            cmd: "reboot",
            data: { dev: 1 }
        }
    }).then(res => {
        if (res.status) fetchDeviceDetail();
    }).finally(() => loading.value = false);
};

设备收到 reboot 指令后执行重启,回复执行结果。服务端收到回复后将该设备从在线列表中移除,等待设备重新登录。

case IotCmds.Reboot:
{
    IotResReboot model = JsonConvert.DeserializeObject<IotResReboot>(msg);
    if (model.Data.R == 0)
    {
        OnlineSocketClients.TryRemove(clientSocket, out _);
        clientSocket.Close();
    }
    break;
}

2. 强制弹宝

可以针对某个槽位弹,也可以全部弹出:

const cmdForce = (v) => {
    const n = v ? v.n : 0;      // 指定槽位,0=全部
    const bsn = v ? v.sn : null; // 电池编号
    loading.value = true;
    cmdSend({
        msg: {
            aims: device.value.aims,
            sn: device.value.sn,
            cmd: "force",
            data: { n, sn: bsn }
        }
    }).then(res => {
        if (res.status) fetchDeviceDetail();
    }).finally(() => loading.value = false);
};

强制弹出是运维的兜底手段。当用户租借后充电宝没弹出来,或者归还后机柜没检测到时,远程弹宝能解决问题。

3. 刷新电池(查询明细)

const cmdDetail = () => {
    loading.value = true;
    cmdSend({
        msg: {
            aims: device.value.aims,
            sn: device.value.sn,
            cmd: "detail",
            data: { n: 0 }
        }
    }).then(res => {
        if (res.status) fetchDeviceDetail();
    }).finally(() => loading.value = false);
};

detail 指令让机柜上报所有槽位的最新状态。设备回复后,服务端解析数据并更新数据库:

public async Task<bool> CmdDetail(IotResDetail model)
{
    // 1. 更新设备库存
    device.ST = st;

    // 2. 删除旧的槽位数据
    efDbContext.ZnDeviceBattery.RemoveRange(bts);

    // 3. 写入最新的槽位数据
    foreach (var item in batteryList)
    {
        var battery = mapper.Map<ZnDeviceBatteryEntity>(item);
        battery.DeviceSN = sn;
        await efDbContext.ZnDeviceBattery.AddAsync(battery);
    }

    await efDbContext.SaveChangesAsync();
}

四、指令下发:IotService.Send 的确认机制

远程控制按钮的背后是 IotService.Send 方法。它做了几件关键的事情:

  1. 找到设备对应的 Socket 连接
  2. 发送指令报文
  3. 记录下发日志
  4. 等待设备回复(轮询确认)
public async Task<ResultModel> Send(IotSendDto iot, bool confirm = true)
{
    // 1. 找到设备 Socket
    var client = GetSocket(sn);
    if (client == null || !client.Connected)
        return new ResultModel(false, "设备离线");

    // 2. 构造报文并发送
    string message = $"#*{strMsg}*#";
    client.Send(Encoding.UTF8.GetBytes(message));

    // 3. 记录下发日志
    await currentRepository.AddAsync(new ZnIotEntity()
    {
        TypeId = 0, // 下发
        SN = sn, Cmd = cmd, Msg = message,
        Result = 0, CreatedDate = DateTime.Now
    });

    // 4. 轮询等待设备回复
    for (int i = 1; i <= interval; i++)
    {
        await Task.Delay(intervalTime);
        result = currentRepository.GetQueryAsNoTracking(
            x => x.SN == sn && x.Cmd == cmd
                 && x.TypeId == 1 && x.Timestamp >= mts).Any();
        if (result) break;
    }

    return new ResultModel(result);
}

confirm 参数控制是否等待设备回复。对于重启、弹宝这类关键操作,confirm = true;对于查询类操作,可以设置为 false 只发不收。


五、心跳监控:设备信号质量的可视化

每台机柜每隔 45 秒发送一次心跳,服务端将心跳数据写入 ZnHeart 表:

public async Task<bool> CmdHeart(IotReqHeart model, string address)
{
    // 写入心跳数据
    var entity = mapper.Map<ZnHeartEntity>(model.Data);
    entity.Id = ZnResponse.GuId();
    entity.SN = model.SN;
    entity.Address = address;
    entity.CreatedDate = DateTime.Now;
    efDbContext.ZnHeart.Add(entity);

    // 更新设备表的心跳时间和信号质量
    var device = await efDbContext.ZnDevice
        .FirstOrDefaultAsync(x => x.SN == model.SN);
    device.Csq = model.Data.Csq;       // 信号质量
    device.HeartTime = DateTime.Now;    // 最后一次心跳
}

在心跳监控页面,运维可以按时间范围查询设备的心跳记录:

信号质量值 csq 越大表示信号越好(0-31),18 以上基本稳定。如果某台设备的 csq 持续偏低,运维需要去现场检查 4G 模块或天线。

心跳数据也有存储期限,配置文件可配:

"TCP": {
    "HeartHours": 72  // 心跳存储时间(小时)
}

定时任务会自动清理过期数据,防止表数据无限膨胀。


六、电池管理:从入库到报废的全生命周期

每一块充电宝都有自己的生命周期——从采购入库、绑定设备、开始使用、到最终报废。

电池列表页面:

租赁次数字段 Times 记录每块电池被租借的总次数,运维可以根据次数判断电池是否需要检修更换。

电池日志:

每次电池流转(创建订单、归还、强制弹出)都会写入 ZnBatteryLog 表,可以查看每块电池的完整操作历史。比如创建订单时的日志:

await batteryLogRepository.AddAsync(new ZnBatteryLogEntity()
{
    Id = ZnResponse.GuId(),
    DeviceSN = model.DeviceSN,
    BatterySN = model.BatterySN,
    Desc = $"创建({entity.OrderSN})",
    CreatedDate = DateTime.Now,
    IsEnabled = 1
});

批量导入:

电池支持 Excel 批量导入。运维从厂商拿到电池数据后,通过导入功能一键入库,系统自动生成唯一的电池编号。


七、IoT 指令日志:全量追溯

所有经过 Socket 的指令交互(下发和接收)都记录在 ZnIot 表中。在 IoT 指令管理页面,运维可以按设备编号、指令类型、时间范围检索。

页面呈现:

Flow 字段用箭头标识指令方向: 表示服务端下发, 表示设备回复。Result 字段用红色/绿色标识执行结果。

设计要点: 每条指令都有一个毫秒级时间戳 Timestamp,用于轮询确认时的匹配。下发和回复通过相同的 SN + Cmd + Timestamp 关联:

// 下发时记录
new ZnIotEntity() { TypeId = 0, SN = sn, Cmd = cmd, Timestamp = mts }

// 回复时记录
new ZnIotEntity() { TypeId = 1, SN = sn, Cmd = cmd, Timestamp = mts }

这样可以通过 TypeId 区分方向,通过 Timestamp 将一对请求-回复关联起来。


八、数据权限在设备管理中的应用

设备管理同样受数据权限控制。商户只能看到自己店铺的设备,运维只能看到负责区域的设备:

if (user?.IsOps == 1)
{
    var codes = JsonConvert.DeserializeObject<List<List<string>>>(areaCode);
    var codeList = new List<string>();
    foreach (var item in codes) { codeList.Add(item.ToJson()); }

    shopIds.AddRange(shopRepository.GetQuery(
        x => codeList.Contains(x.AreaCode)).Select(x => x.ShopId));

    strWhere = strWhere.And(x => shopIds.Contains(x.ShopId));
}

这让不同角色的运维人员登录后看到不同的设备范围,互不干扰。


九、小结

设备管理模块是运维人员的日常工作台。核心功能就几点:

  1. 设备列表 + 在线状态——实时反映设备连接状况
  2. 远程操控——重启、弹宝、明细查询,减少现场运维成本
  3. 心跳监控——通过信号质量数据判断设备运行状态
  4. 电池管理——从入库到报废的全生命周期追踪
  5. IoT 指令日志——全量记录,方便排查问题

下一篇会讲缓存策略、审计日志、定时任务与授权保护,看看我们怎么用 AOP 拦截器、Quartz 任务调度和 License 机制来保障平台稳定运行。

有疑问或者想讨论的点,留言区见。

天草工坊公众号

欢迎关注 天草工坊 公众号

好的设计被看见,好的开发被落地。

围观:28| 收获点赞:0

评论 0

暂无评论,来说点什么吧

?
内容目录
快捷操作
点赞 评论 返回顶部