☰
Go与Odoo构建物联网平台:从设备数据采集到自动生成维修工单
2026/10/11 5:09:04 网站建设 项目流程

1. 项目背景与整体设计思路

做物联网平台这件事,我在不同阶段踩过不少坑。早期用某云厂商的物联网套件,设备接入倒是快,但问题出在数据闭环上——设备报警了,告警推送停在手机App里,运维人员看完还得手工去ERP系统登记维修单,一忙起来就漏登记,尤其夜班报警,第二天早上才发现设备已经带病跑了一整夜。

所以当某工厂找到我,想做一个能覆盖“设备状态采集—异常识别—自动生成维修工单”全链路的内部平台时,我几乎没犹豫就确定了技术组合:Go 写物联网接入与业务服务,Odoo 承担 ERP 侧的设备档案、维修工单和备件管理。核心目标只有一个:设备数据到了,异常判断做完,维修单能自动落到 ERP 系统里,全程不需要人手工搬运数据。

这个标题描述的场景,本质上解决的是物联网系统与ERP系统之间的数据孤岛问题。设备产生了异常信号,如果只是在物联平台弹一条告警,价值和一条手机推送没区别;真正有价值的是让告警直接驱动企业业务流程——生成维修单、通知维修班组、关联设备备件、跟踪维修闭环。

1.1 为什么选 Go + Odoo,而不是其他组合

先说 Go。我在选型时也犹豫过要不要用 Java 或者 Node.js,但最终 Go 胜出的理由非常实际:

  • 并发模型适合设备接入。一个工厂几百台设备,每台设备几秒上报一次数据,意味着每秒几百上千条消息进来,Go 的 goroutine 处理这种高并发 IO 场景非常自然,代码写起来也不用像 Java 那样堆线程池配置。
  • 部署简单。编译出来就是一个二进制文件,丢到服务器上就能跑,对现场运维人员非常友好。某工厂的服务器环境比较老旧,没有 Docker,Java 项目光装 JRE、配环境变量就折腾半天,Go 完全没有这个问题。
  • 内存占用低。这一点在工控机上尤其重要,很多边缘网关内存只有几百MB,Go 程序跑起来占用资源比 Java 少一个量级。

再聊 Odoo。当时也考虑过用泛微、SAP 之类的大型 ERP,但某工厂的需求是:维修工单流程灵活、要能对接外部 API、预算有限、希望业务人员直接网页操作。Odoo 的吸引力在于:

  • 开源社区版免费,功能够用。设备管理、工单管理、库存管理这些模块开箱即用。
  • API 成熟。支持 JSON-RPC 和 XML-RPC 两种外部调用方式,方便 Go 服务集成。
  • 工作流可配置。维修单的审批、派工、结单流程可以通过 Odoo Studio 或直接在代码里调整,不用每次改动都找厂商。

这个组合的定位是:Go 负责实时数据处理和业务编排,Odoo 负责企业流程和状态沉淀,各干各擅长的事。

1.2 系统架构与核心流程

整个系统的架构可以概括成“三横一纵”:

  • 接入层:设备通过 MQTT 协议上报数据到 EMQX Broker,Go 服务作为 MQTT 客户端订阅所有设备主题,实时消费数据。
  • 处理层:Go 服务完成数据解析、异常检测、告警判定,并将设备实时状态同步到 Redis,历史数据批量写入 时序数据库(这里用的某开源时序库)。
  • 业务层:Go 服务通过 Odoo 外部 API 创建设备维修工单,触发 Odoo 内部的消息通知,把维修任务分配到对应班组。

核心流程是这样的:

设备上报数据 -> MQTT Broker -> Go 服务消费消息 -> 异常检测引擎判断 -> 正常:仅更新 Redis 设备状态 -> 异常:写入告警记录,调用 Odoo API 创建维修单 -> Odoo 维修单创建成功 -> 回写维修单号到平台 -> 通知对应维修人员

这个流程里最关键的设计点是把实时处理和历史记录分开。设备数据量大,如果每一条都直接写数据库,磁盘和数据库压力都会很大;但如果只依赖内存判断,又没法追溯历史趋势。所以做了分层:实时判断走内存和 Redis,历史数据异步批量落库,保证热路径的响应速度。

1.3 关键模块拆解

整个系统拆成六个模块,每个模块的职责边界非常清晰:

模块职责关键点
设备接入服务MQTT 订阅、消息解析、协议适配多协议兼容、断线重连
实时数据处理数据清洗、单位换算、数据补全容错处理脏数据
异常检测引擎阈值判断、趋势判断、状态机规则可配置、防误报
告警中心告警生成、告警分级、升级策略避免告警风暴
Odoo 集成服务工单创建、设备关联、状态回写API 调用、幂等控制
运维看板设备状态总览、异常趋势、工单跟踪数据可视化

每个模块都可以独立部署、独立升级。比如刚开始异常检测引擎只做了简单的阈值判断,后来逐步加了趋势分析和状态机,这个过程不用动其他模块,只要保证接口没破坏就行。

2. 设备接入与数据链路搭建

2.1 设备端协议选型:为什么是 MQTT

物联网设备接入的协议选择,其实就那么几种:HTTP 轮询、CoAP、MQTT、Modbus TCP 直连。我最后选了 MQTT,原因是它天然适合“大量设备 + 低带宽 + 不稳定的网络环境”这种场景。

MQTT 是发布订阅模式,设备只负责发布消息到主题,服务端只负责订阅主题消费消息,设备不用关心服务端的 IP 和端口(除了 Broker),这对现场网络环境复杂、设备经常移动的场景非常友好。

某工厂的设备用的是某款工业网关,支持 MQTT 协议上报 JSON 格式的数据。每台设备会上报几个主题:

  • factory/device/{device_id}/telemetry:实时遥测数据,包括温度、压力、振动、转速等。
  • factory/device/{device_id}/status:设备启停状态、故障码。
  • factory/device/{device_id}/event:设备主动上报的事件,比如急停、开门、参数变更。

设备侧配置比较简单,无非是 Broker 地址、端口、ClientID(用设备唯一编号)、用户名密码、上报周期。这里特别提醒一下:ClientID 必须全局唯一,否则多个设备同时在线时会发生互相踢下线的问题,这是 MQTT 协议的特性,踩过坑的人都知道这个痛苦。

2.2 Go 接入服务设计与实现

Go 这边我用的是某开源 MQTT 客户端库,支持 QoS 0/1/2 三种消息质量级别,重连机制也比较完善。

核心代码逻辑其实不复杂,但有几个细节需要注意:

// MQTT 消息处理的核心结构体 type MQTTService struct { client mqtt.Client deviceSvc *DeviceService alarmEngine *AlarmEngine odooSvc *OdooService } // 订阅消息的回调函数 func (s *MQTTService) handleMessage(_ mqtt.Client, msg mqtt.Message) { // 1. 从主题中解析设备ID deviceID, err := parseDeviceIDFromTopic(msg.Topic()) if err != nil { logrus.WithField("topic", msg.Topic()).Error("解析设备ID失败") return } // 2. 解析消息内容(JSON格式) var payload map[string]interface{} if err := json.Unmarshal(msg.Payload(), &payload); err != nil { logrus.WithField("device_id", deviceID).Warn("设备上报数据解析失败") return } // 3. 设备数据标准化处理 metric, err := s.deviceSvc.Standardize(deviceID, payload) if err != nil { logrus.WithField("device_id", deviceID).Error("数据标准化失败") return } // 4. 送入实时判断引擎(异步,避免阻塞消息循环) s.alarmEngine.ProcessMetric(metric) }

这个结构最大的好处是消息处理不走阻塞路径,ProcessMetric内部是异步的,这样即使某台设备的处理逻辑很慢,也不会拖累整个系统的吞吐量。

但这样设计也有一个坑:异步处理在系统重启时容易丢消息。MQTT QoS 1 能保证消息不丢失但可能重复,所以在落库之前必须做去重处理。我的方案是:每条消息带一个自增序号(seq),设备每 5 秒上报一次,序号单调递增。Go 服务在 Redis 里维护每个设备最近一条消息的 seq,新消息的 seq 如果小于等于已处理的 seq,直接丢弃。

2.3 设备数据标准化与存储

设备上报的数据格式五花八门,有的字段叫temp,有的叫temperature,有的上报的是华氏度,有的是摄氏度,还有的单位是 kPa 但系统里要换算成 MPa。所以我做了数据标准化层,核心逻辑是:

  • 统一字段命名:所有设备的遥测数据统一转换为temperature、pressure、vibration、rotation_speed四个核心指标。
  • 统一单位:温度统一为摄氏度,压力统一为 MPa,振动统一为 mm/s。
  • 统一精度:浮点数统一保留两位小数。

这个标准化层非常关键,后续所有的异常检测规则、阈值配置、告警展示都基于标准字段,不用为每个设备单独写判断逻辑。配置层面用一张映射表维护:

# 设备字段映射配置示例 device_type: "centrifuge" field_mapping: temp: "temperature" press: "pressure" vib: "vibration" speed: "rotation_speed" unit_conversion: press: "kPa_to_MPa" # 数值除以1000

数据存储分两层:实时数据存 Redis(TTL 设置为 24 小时),历史数据异步写入时序库。时序库的写入策略是批量写入,每 5 秒 flush 一次,减少 IO 压力。实际跑下来一个月的数据量大概 2TB 左右(300 台设备每 5 秒一条),时序库的压缩比不错,实际磁盘占用约 300GB,完全在可接受范围。

3. 异常检测与告警规则设计

这是整个平台最核心的部分,也是用户满意度影响最大的模块。如果异常检测太灵敏,一天几百条告警,运维人员会直接忽略所有告警;如果太迟钝,设备坏了半天没人发现,自动生成工单就失去了意义。

3.1 阈值检测与动态基线

最开始做的是最简单的阈值判断:温度超过 80 摄氏度就报警,压力超过 1.2 MPa 就报警。但在实际测试中发现静态阈值完全不够用:

  • 离心机在启动瞬间转速从 0 直接到 3000 转/分钟,振动值会有一个尖峰,如果阈值设低了会误报。
  • 不同设备的正常工况不一样,同型号设备在不同原料批次下的温度基线也不同。
  • 夏天环境温度高,设备散热差,正常运行温度可能比冬天高 10 度。

所以后来引入了动态基线算法。基本思路是:以过去 24 小时的数据为样本,计算每个时间段(按小时)的均值和标准差,用“当前值 > 均值 + 3倍标准差”作为异常判据。这个方案我参考了一些时序异常检测的实践,实现起来也不复杂。

// 动态基线异常判断核心逻辑 func (e *AlarmEngine) checkDynamicBaseline(deviceID string, metric string, value float64) bool { // 获取过去24小时的样本数据(按小时分组) samples := e.metricCache.GetLast24Hours(deviceID, metric) hour := time.Now().Hour() hourSamples := samples[hour] if len(hourSamples) < 30 { // 样本不足,退化为静态阈值 return value > e.getStaticThreshold(metric) } mean := calculateMean(hourSamples) stddev := calculateStddev(hourSamples) // 当前值超过均值+3*标准差,判定为异常 threshold := mean + 3*stddev return value > threshold }

这个方案跑了一段时间后,误报率显著下降。但动态基线也有个问题:如果设备已经坏了很多天,坏数据会被当成正常基线。所以我又加了一个约束:基线样本只取“设备健康状态”时段的数据,如果设备当时正处于告警状态,该时段数据不参与基线计算。

3.2 告警分级与升级策略

告警不能全一个级别,否则关键时刻分不清轻重。我按照设备故障的严重程度和影响范围,设置了四级告警:

级别含义响应时限示例
P0紧急停机、安全风险5分钟电机超温、急停触发
P1严重故障、影响生产30分钟振动严重超标、压力过高
P2一般故障、需监控4小时温度偏高、运行效率下降
P3提示信息24小时润滑周期到期、参数漂移

不同级别的告警触发的动作不一样:

  • P3只在平台内记录,不生成维修单,减少噪音。
  • P2生成维修单并通知班组长,要求在 4 小时内确认。
  • P1生成维修单,同时通过短信和电话语音通知设备主管。
  • P0生成维修单,同时触发设备安全联锁(通过反向 MQTT 指令让设备降载或停机)。

告警升级策略也很重要。比如 P2 告警生成了维修单,但 4 小时内没有人接单,系统会自动升级到 P1,重新分配维修班组并通知上一级主管。这个策略是用 Odoo 的定时任务(Cron Job)实现的,每 5 分钟扫描一次未接单的工单。

3.3 避免误报的三道闸门

误报是告警系统最大的敌人,实际运行中我做了三道防线:

第一道:短时滤波。瞬时跳变不触发告警,必须是连续 3 次采样(15 秒)都超过阈值才确认异常。这个机制过滤掉了传感器本身噪声和瞬时干扰。

第二道:交叉验证。同一设备如果有多个传感器,异常信号需要至少两个指标同时超限才触发。比如振动值高,同时转速异常,才判定为机械故障;只有振动值高但转速正常,可能只是传感器本身接触不良,标记为疑似传感器故障,不生成维修单。

第三道:人工复核窗口。这个设计比较巧妙——设备产生告警后,系统不会立即生成维修单,而是先进入 30 秒的“确认窗口”。在这 30 秒内,如果后续数据恢复正常,告警自动撤销;如果继续异常,才触发维修单创建。这个逻辑借鉴了断路器模式,效果非常好,能过滤掉不少一次性扰动。

但人工复核窗口带来的一个问题是:维修单生成时间延后了 30 秒。对于 P0 级告警,这个延迟不能接受。所以 P0 级别不走确认窗口,直接创建维修单并触发联动。

4. 对接 Odoo 生成维修单的实现细节

4.1 Odoo 外部 API 的两种对接方式

Odoo 对外提供两种远程调用接口:XML-RPC 和 JSON-RPC。我一开始用的 XML-RPC,因为网上资料多、案例多,很多老项目都是用这种协议对接的。但实际用下来发现 XML-RPC 的调试很痛苦,返回的数据结构比较啰嗦,而且 Python 生态之外的客户端库支持不够好。

后来切换到 JSON-RPC,同样用execute_kw方法,Go 侧只需要发一个 HTTP POST 请求就行了,数据结构清晰,调试也方便。Odoo 从 13 版本开始 JSON-RPC 的接口已经很稳定了。

调用 Odoo 创建维修单的交互流程:

Go 服务 -> POST /web/dataset/call_kw -> 认证(API 密钥) -> 调用维修单模型的 create 方法 -> 传入工单数据(设备ID、故障描述、级别、报修人) -> 等待 Odoo 返回新工单 ID

4.2 用 Go 调用 Odoo API 创建维修单

这里给出一个核心的代码片段,展示 Go 服务如何调用 Odoo 的 JSON-RPC 接口创建维修单:

package odoo import ( "bytes" "encoding/json" "fmt" "net/http" "time" ) // OdooClient 是 Odoo API 的客户端封装 type OdooClient struct { BaseURL string DB string Username string Password string UID int HTTPClient *http.Client } // CallKw 调用 Odoo 的通用 JSON-RPC 方法 func (c *OdooClient) CallKw(model string, method string, args []interface{}, kwargs map[string]interface{}) (interface{}, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "method": "call", "params": map[string]interface{}{ "model": model, "method": method, "args": args, "kwargs": kwargs, "context": map[string]interface{}{ "lang": "zh_CN", }, }, } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", c.BaseURL+"/web/dataset/call_kw", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(c.Username, c.Password) resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("调用Odoo接口失败: %w", err) } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } if result["error"] != nil { return nil, fmt.Errorf("Odoo返回错误: %v", result["error"]) } return result["result"], nil } // CreateMaintenanceOrder 创建维修单 func (c *OdooClient) CreateMaintenanceOrder(params MaintenanceOrderParams) (int, error) { values := map[string]interface{}{ "x_device_id": params.DeviceID, "x_device_name": params.DeviceName, "x_fault_desc": params.FaultDesc, "x_alarm_level": params.AlarmLevel, "x_repair_team": params.RepairTeam, "x_source": "iot_platform", // 标记来源,方便追踪 "x_platform_alarm_id": params.AlarmID, // 平台侧告警ID,做幂等用 "name": fmt.Sprintf("[IoT] %s - %s", params.DeviceName, params.FaultDesc), } result, err := c.CallKw("x_maintenance.order", "create", []interface{}{values}, map[string]interface{}{}) if err != nil { return 0, err } // Odoo 的 create 返回新记录的 ID if id, ok := result.(float64); ok { return int(id), nil } return 0, fmt.Errorf("Odoo返回格式异常: %v", result) }

这里我用的是 Odoo 的x_自定义字段,而不是标准的维修工单模型(maintenance.request),因为某工厂的工单流程比较特殊,需要挂接设备编号、告警级别、平台告警 ID 等自定义属性。在 Odoo 里通过“开发者模式->技术->字段”创建这些自定义字段,过程不算复杂但需要点耐心。

身份证级的建议:所有通过 API 对接的字段最好都加上 x_ 前缀,这样一眼就能看出是从外部系统同步过来的字段,避免和 Odoo 原生的name、date_deadline这些字段混淆。另外记得给这些字段加上索引,否则数据量上来后查询会非常慢。

4.3 内外账号联动与状态同步

设备告警自动生成的维修单,最终是要让维修班组去处理的。这里有个关键问题:维修班组的人不一定会登录物联网平台,他们只会在 Odoo 里看工单。

所以我的做法是:

  • 工单会显示一个来源标签[IoT],同时显示设备编号和故障描述。
  • 维修人员只需要在 Odoo 里完成接单、填写处理情况、关闭工单。
  • Odoo 侧通过一个服务器动作(基于webhook或cron)将工单状态变更推送到 Go 服务(或由 Go 服务定时轮询)。
  • Go 服务拿到最新状态后,更新本地告警的状态——比如“维修中”、“已修复”等。

状态同步的方式我最初用的是 Odoo webhook,Odoo 在工单状态变化时调用 Go 服务的 HTTP 接口。但实际运行中发现 Odoo 的 webhook 不稳定,经常有消息丢失的情况。后来改成了 Go 服务每分钟轮询一次 Odoo 接口,获取最近变更的工单记录,再同步到本地。轮询虽然有一点延迟,但保证不丢数据,可靠性优先。

状态同步需要注意:防止循环调用。Go 服务创建了维修单,维修单状态变化时 Odoo 通知 Go 服务,Go 服务又更新告警,这个链路中要加一个标识,避免 Odoo 状态变化回调又触发 Go 服务去创建新的工单。我的方案是在创建工单时设置x_source = iot_platform,Go 服务只在创建新工单时检查这个标记,不会因为维修单状态变化再创建新的维修单,从而打断了循环。

5. 常见问题与排查技巧实录

项目上线不是终点,稳定运行才是考验。这半年里遇到的各种问题,挑几个有代表性的分享出来,希望能帮后来者少走弯路。

5.1 MQTT 断线重连与消息积压问题

上线初期遇到最头疼的问题就是 MQTT 断线。现场的网络环境比办公室复杂得多,设备经常因为网络抖动和 Broker 断开连接。而且有些网关设备的 MQTT 客户端实现不完善,重连逻辑写得不好,经常出现“假死”状态——设备看起来在线,但已经停止了数据上报。

排查过程:

  1. 首先在 Broker 侧开启了 WebHook,监听设备上下线事件。
  2. 然后用一个定时任务每 10 分钟检查一次每台设备的最后上报时间,如果超过 3 个上报周期没有新数据,判定设备“失联”,在平台标记为离线。
  3. 人工去现场查看那台网关,发现是网关的 TCP 长连接没有设置心跳,被中间的路由器 NAPT 表项清除后连接断开了,但网关不自知,也不主动重连。

解决方案是让设备侧每 30 秒发送一次 MQTT PingReq(心跳包),并将 MQTT KeepAlive 参数设置为 60 秒。如果 3 次心跳没有收到 PingResp 就主动重连,重连的时间退避策略是:1 秒、2 秒、4 秒、8 秒……最长 60 秒,避免同时大量设备重连造成 Broker 压力过大。

遇到消息积压的问题:某次 Broker 挂了 20 分钟重启,积压了约 1 万条设备消息。Go 服务恢复消费后,由于处理速度跟不上,在 Redis 里积压了大量待处理数据。这个问题的教训是:一定要在下游存储(时序库、Redis)的 IO 路径上做熔断和限流。后来我在时序库写入层加了令牌桶限流,同时扩充了消费者 goroutine 的数量(从默认的 4 个扩展到 16 个)。

5.2 消息乱序与幂等控制

MQTT 的 QoS 1 保证消息至少送达一次,但不保证顺序,也不保证不重复。实际运行中确实遇到了几次乱序问题:

  • 某设备上报温度:先报 90 度(异常),再报 95 度(异常)。但因为网络原因,90 度这条消息后到了。异常检测引擎先看到 95 度,触发了 P1 告警,然后 90 度消息到达,判断结果也是异常,又触发了一次 P1 告警,重复生成两张维修单。

解决思路分两层:

第一层,按设备串行处理。Go 接入服务里给每台设备分配一个独立的 goroutine(带缓冲的 channel),同一台设备的消息严格按 seq 序号排序后逐个处理。Redis 里记录每个设备最后处理的 seq,新消息 seq 小于等于记录的直接丢弃。这样从源头上保证进入检测引擎的消息是有序且不重复的。

第二层,检测侧幂等。告警检测引擎里维护设备的当前状态(正常/告警),只有在状态发生转换时才触发新的告警动作。比如设备持续超温,只会在第一次超温时创建维修单,后续的超温数据不会继续触发。只有当这个告警解除(设备恢复正常)后,再次发生超温才会创建新维修单。这个“状态机”设计避免了很多重复工单。

5.3 Odoo 接口调用的几个经典坑

第一个坑是 Odoo 的日期字段格式。通过 JSON-RPC 传日期,必须用 ISO 8601 格式,而且 Odoo 对时区非常敏感。如果直接传2024-08-01 15:30:00,Odoo 会把它当成 UTC 时间,东八区的用户看到的时间会多 8 个小时。解决办法是在请求上下文中设置tz为Asia/Shanghai。

第二个坑是 Odoo 的create方法返回 ID 时的类型问题。在 JSON-RPC 中返回的整数在 Go 的interface{}里很可能被解析成float64,这就是我在代码里做的类型断言result.(float64)的原因。这个小问题曾经让我排查了半天,最后用日志打印%T才发现类型不匹配。

第三个坑是 Odoo 的权限模型。外部 API 调用时用的是 API 账户,如果没有对应的访问权限,create操作会静默失败或报权限错误。而且 Odoo 的“记录规则”(Record Rules)也会影响可见性,默认情况下外部 API 账户只能访问自己创建的数据,需要到“设置-安全-记录规则”里针对x_maintenance.order模型配置全局可见和全局创建权限。

5.4 常见问题速查表

问题现象可能原因排查步骤解决方案
设备显示离线但网关正常MQTT 心跳设置不合理查看 Broker 在线列表设备侧配置 KeepAlive=60s,启用主动重连
维修单重复创建消息乱序或重复Redis 查询设备最近 seq按设备串行处理 + 告警状态机
工单日期相差 8 小时时区配置不一致查看请求上下文JSON-RPC 请求体中设置tz: "Asia/Shanghai"
Odoo API 返回权限错误API 账户权限不足查看 Odoo 日志给 API 账户配置模型级读写权限
历史数据查询缓慢时序库未建立标签索引EXPLAIN 查询计划为 device_id 和 metric 字段建索引
告警太多太频繁阈值设置太敏感分析告警趋势调高阈值倍数,加入短时滤波

这里再分享一个独门排错技巧:在 Go 服务里给每一条关键路径都打上结构化日志(设备ID、消息ID、处理耗时)。刚开始觉得很繁琐,但真正排查问题时,这些日志的价值远超预期。生产环境我直接用文件收集日志,写入本地并推送到日志分析平台做检索。比较好的效果是某次 Odoo 接口响应变慢,从日志里看到调用耗时从 50ms 涨到了 800ms,提前发现了 Odoo 数据库连接池打满的问题。

6. 项目上线后的实际效果与优化空间

6.1 上线前后的数据对比

这个系统上线稳定运行了三个月后,我做了一次数据对比,效果很明显:

指标上线前上线后
设备异常发现到生成维修单的时间平均 2.5 小时(人工发现)平均 30 秒(自动检测)
维修单漏登记率约 15%(夜班尤其严重)0%
平均维修响应时间4 小时1.5 小时
设备非计划停机次数月均 5 次月均 2 次

最典型的一次:某台压缩机在凌晨 2 点发生振动异常,系统在 35 秒内生成了 P1 级维修单并短信通知了设备主管,维修班组早上 6 点就完成了轴承更换。放在以前,这种夜间故障最快也要等到早上巡检才能发现,至少损失半天生产时间。

但这个项目也有很多可以优化的地方,这也是我想重点说的:任何事情都是迭代出来的。

6.2 项目局限与后续优化思路

第一,告警规则的配置化程度还不够。目前规则还是写在 Go 代码里的,调整阈值需要改代码重新编译发布。对运维人员来说,最好是在管理后台可视化配置规则。我建议后续可以引入规则引擎(比如把规则存在 时序数据库 或独立的 MQTT 主题里),通过配置下发的方式动态更新。

第二,Odoo 侧还可以做更多自动化。目前只是自动创建了维修单,但后续的维修进度跟踪(比如备件领用、工时填报)还是人工在 Odoo 里操作。理论上可以在 Go 服务里做库存联动——当维修单关联了备件更换,自动从 Odoo 库存中预留或扣减对应备件,这部分流程跑通后,备件管理的效率还能提升一大截。

第三,设备预测性维护是个大方向。目前的异常检测是基于实时数据的反应式判断,也就是“发生异常了才知道”。后续可以基于时序数据做趋势预测,比如振动值连续一周上升,虽然还没超过阈值,但预测未来 3 天可能发生故障,提前安排计划性检修。这样能把“被动维修”变成“主动维护”,大幅降低非计划停机时间。

第四,边缘计算能力还没用起来。现在所有设备数据都往云端传,如果直接在边缘网关侧做初步的异常预判(比如简单的阈值和趋势判断),只在异常时才向云端上报,可以大幅降低网络带宽和云端计算压力。这也是后续可以考虑的方向。

6.3 给后来者的几点建议

如果看文章的你正准备做类似的物联网+ERP 集成项目,我个人的经验建议是:

先理业务流程,再定技术架构。物联网平台本身好做,难的是理解企业实际的维修流程。一定要花时间去设备现场看一遍,和维修师傅聊一聊:设备多久保养一次?故障报修是走什么流程?谁负责审批?谁负责接单?只有把这些业务流程吃透了,才能在 Odoo 里做出真正符合实际的操作界面和工作流。

数据结构设计要留出扩展余地。设备类型会变、传感器会加、业务字段会调整,数据库设计时不要过度规范化导致后面扩展困难。我在设备遥测数据表里用 JSON 字段存储附加指标,虽然违背了“规范化”的理念,但换来的是极强的灵活性。

小步快跑,先做最核心的闭环。第一个版本不用做得很完美,能跑通“设备异常 -> 自动生成维修单”这个核心闭环就够了。等用户用起来、反馈来了,再逐步加功能、优化体验。我第一版只有阈值告警和工单自动创建两个功能,上线用了两周后才有信心逐步增加动态基线、告警分级、状态同步这些进阶能力。

技术选型和架构设计都是可以讨论和调整的,但有一个原则不能变:系统最终是给现场用户用的,用户觉得好用、愿意用,才是真正的成功指标。这套 Go + Odoo 的平台,从设备数据接入到 ERP 工单闭环,技术本身没什么稀奇,但确实解决了工厂真实的问题,减少了设备故障的漏发现、漏登记,缩短了维修响应时间。这种“把对的技术用在真正有价值的地方”的成就感,大概就是我坚持做这块的原因吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询