一、引言
充电宝机柜分布在城市的各个角落,运维人员不可能天天跑现场。所以后台必须提供一个「远程控制中心」——能看到每台设备的在线状态、信号强度、电池槽位详情,还能远程重启、强制弹宝、查询明细。
这一篇就讲设备管理模块的几个核心功能:设备列表与数据导入、远程指令操控、设备详情实时面板、心跳数据监控,以及 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 方法。它做了几件关键的事情:
- 找到设备对应的 Socket 连接
- 发送指令报文
- 记录下发日志
- 等待设备回复(轮询确认)
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));
}
这让不同角色的运维人员登录后看到不同的设备范围,互不干扰。
九、小结
设备管理模块是运维人员的日常工作台。核心功能就几点:
- 设备列表 + 在线状态——实时反映设备连接状况
- 远程操控——重启、弹宝、明细查询,减少现场运维成本
- 心跳监控——通过信号质量数据判断设备运行状态
- 电池管理——从入库到报废的全生命周期追踪
- IoT 指令日志——全量记录,方便排查问题
下一篇会讲缓存策略、审计日志、定时任务与授权保护,看看我们怎么用 AOP 拦截器、Quartz 任务调度和 License 机制来保障平台稳定运行。
有疑问或者想讨论的点,留言区见。