C++ 工业数据服务后端项目复盘:从高频数据写入到实时报警推送

1. 项目背景

这个项目是一个面向工业设备和上位机系统的数据服务后端,主要负责接收电解槽、MEA、CCM 等生产和测试环节产生的数据,并将这些数据稳定写入 PostgreSQL 数据库。

系统除了基础的数据入库,还提供了动态查询、更新、删除、用户登录认证、设备写入权限控制、实时报警推送等能力。整体目标是让前端大屏、上位机、生产管理系统能够通过统一的 HTTP API 访问后端数据。

2. 技术栈

模块 技术
开发语言 C++17
HTTP 框架 Pistache
数据库 PostgreSQL
数据库访问 libpq 原生 C API
JSON 处理 nlohmann/json
并发处理 自定义 ThreadPool、后台 Worker
数据写入 批量队列、事务提交、失败降级
认证 JWT、libsodium 密码校验
日志 spdlog
实时推送 SSE
外部通知 libcurl、飞书机器人 Webhook
构建系统 CMake
配置管理 JSON 配置文件
扩展依赖 OpenCV

这套技术栈的特点是比较偏底层,尤其是数据库访问没有使用 ORM,而是直接基于 libpq 封装,这样可以更明确地控制 SQL、连接生命周期和性能开销。

3. 系统整体架构

系统可以分成四层:

系统架构图

HTTP 层负责接收请求和路由分发,Handler 层负责业务处理。查询类接口通过数据库连接池访问 PostgreSQL,写入类接口则先进入内存队列,再由后台 Worker 批量写入数据库。

这样设计的好处是:上位机上报数据时不需要等待数据库完整写入,可以快速返回;后台再统一进行批处理,提高吞吐能力。

4. 核心模块设计

4.1 配置管理

项目通过 config/config.json 管理运行参数,包括服务端口、线程数、数据库连接串、JWT 参数、写入队列大小、批量写入大小、飞书机器人地址等。

这样可以避免把环境参数写死在代码里,方便在开发、测试、部署环境中切换。

4.2 元数据驱动的数据映射

项目中有一个比较关键的配置文件:metadata.json。

它定义了逻辑表名、真实数据库表名、主键、字段映射和写入白名单。例如前端或上位机传入的是中文字段名,后端会根据元数据映射到数据库字段。

这种设计带来了几个好处:

  • 前端字段和数据库字段解耦;
  • 动态 CRUD 可以通过白名单限制表和字段;
  • 不同业务表可以复用同一套写入逻辑;
  • 设备写入权限可以在配置层控制。

4.3 批量写入引擎

数据写入没有直接在 HTTP 请求线程里执行,而是进入 BatchIngestor 队列。

写入流程大致是:

  1. 上位机调用 /api/test_data;
  2. 后端校验 JWT,并提取 client_id;
  3. 根据 metadata.json 校验表名和写入权限;
  4. 将数据转换成 RowTask 放入队列;
  5. 后台 Worker 从队列中批量取数据;
  6. 按表分组;
  7. 使用事务批量写入 PostgreSQL;
  8. 如果批量写入失败,则降级为逐条写入。

数据写入流程图:

数据写入流程图

这个设计解决了高频数据上报时 HTTP 请求被数据库阻塞的问题,也避免了单条插入带来的性能浪费。

4.4 数据库连接管理

查询接口使用连接池,连接通过 RAII 的方式自动归还。每次获取连接时会检查连接是否有效,如果连接断开,会尝试重连或重建连接。

写入 Worker 则使用独立长期连接,避免后台写入线程和前台查询线程争抢同一个连接池。

4.5 动态 CRUD

系统提供了通用接口:

  • POST /api/query
  • PUT /api/update
  • POST /api/delete

这些接口不是直接拼接任意 SQL,而是先通过 metadata.json 校验逻辑表和字段,再由后端生成参数化 SQL。

这样既保留了动态查询的灵活性,也降低了 SQL 注入风险。

4.6 JWT 认证与设备权限

登录接口 /api/login 支持普通用户和上位机设备两种模式。

普通用户 Token 使用配置文件中的过期时间;上位机 Token 会额外带上 client_id,有效期默认一年。

设备上报数据时,后端会读取 Token 中的 client_id,再和 metadata.json 中的 write_permission 进行匹配。这样可以限制不同设备只能写入自己被授权的数据表。

4.7 实时报警推送

报警模块不是直接在数据写入逻辑里硬编码判断,而是通过 PostgreSQL 的 LISTEN / NOTIFY 机制接收数据库侧产生的报警事件。

后端的 PgListener 会监听 electrolyzer_issue_channel。当数据库发出通知时,后端解析报警 JSON,然后交给 AlarmManager 处理。

AlarmManager 做了三件事:

  • 通过 SSE 推送给前端大屏;
  • 通过飞书机器人发送外部通知;
  • 对相同 rate_limit_key 做 20 秒限流,避免重复报警刷屏。

报警链路图:

报警链路图

5. 项目中的工程思考

这个项目里比较有价值的点,不是单独某个接口,而是整体工程结构:

第一,写入链路做了异步化。HTTP 请求只负责接收和入队,数据库写入交给后台 Worker,降低了上位机请求超时的风险。

第二,使用元数据驱动表结构。新增业务表时,可以更多地通过配置扩展,而不是复制一套新的 Handler。

第三,数据库操作尽量做了安全边界。动态查询、更新、删除都要求表名和字段经过白名单校验,避免完全开放 SQL 能力。

第四,报警链路和业务写入链路解耦。报警由数据库通知触发,后端负责转发和限流,这样后续扩展短信、邮件、企业微信等渠道也比较自然。

6. 当前不足与后续优化

这个项目目前已经具备核心能力,但仍然有一些可以继续优化的地方:

  • 部分接口目前还没有统一接入 JWT 鉴权,例如 Flex CRUD 接口;
  • 写入队列是内存队列,服务异常退出时可能丢失未落库数据;
  • 日志配置中有滚动日志参数,但当前代码还没有真正接入 rotating sink;
  • server.host 配置目前被读取,但实际服务绑定的是 0.0.0.0;
  • OPTIONS 预检路由还不是全路由自动处理;
  • 后续可以考虑增加单元测试、接口测试和部署脚本。

7. 总结

这个后端项目主要完成了工业数据服务中的几个关键问题:高频数据接入、批量写入、动态查询、权限控制、实时报警和外部通知。

对我来说,这个项目的价值不仅在于实现了一组 API,更重要的是完整经历了一个后端服务从路由设计、配置管理、数据库封装、并发写入、安全控制到报警推送的工程化过程。

它也让我更清楚地理解了:在实际业务系统里,稳定性、可维护性和可扩展性往往比单个功能点更重要。一个好的后端服务,不只是能把数据写进去,还要能在高频请求、异常数据、数据库波动、权限隔离和报警通知这些场景下持续工作。