2026-07-02 05:38:01 +08:00
|
|
|
|
//! 设备消息处理逻辑(轻量化)。
|
|
|
|
|
|
//!
|
2026-09-19 23:40:46 +08:00
|
|
|
|
//! 设备服务只做**协议透传**,不处理业务逻辑:
|
|
|
|
|
|
//! - `status_post` 的通道数据在设备侧已是 **18 字符 hex 明文**,本服务**原样透传**,
|
|
|
|
|
|
//! 不再做任何拆解/位域解析(解析归 api-server 的业务层)
|
2026-07-02 05:38:01 +08:00
|
|
|
|
//! - 所有设备上报消息统一转发到 Redis `device:reports` 队列
|
|
|
|
|
|
//! - 不写 DB、不验证签名、不处理业务规则
|
|
|
|
|
|
//! - auth_str/login 等协议级消息也由 API 服务处理后通过 `device:commands` 回复
|
|
|
|
|
|
|
|
|
|
|
|
use super::connection::ConnectionPool;
|
|
|
|
|
|
use super::protocol::DeviceMessage;
|
|
|
|
|
|
|
2026-09-20 01:21:17 +08:00
|
|
|
|
/// 上报队列 `device:reports` 的**长度上限**(超过就裁剪最旧的)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// **为什么必须有上限**:本服务只 `LPUSH`,api-server 是唯一消费者;一旦 api-server 长时间不消费
|
|
|
|
|
|
/// (宕机/卡死/上线顺序颠倒),队列会无界增长,而 Redis 实例 `maxmemory=1GB`、
|
|
|
|
|
|
/// `maxmemory-policy=volatile-lru` 且实例上绝大多数 key 没有 TTL(可淘汰集合≈1)⇒
|
|
|
|
|
|
/// **写命令 OOM 会连累同实例上既有的充电站生产平台**。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// **为什么取 200000**(估算依据):
|
|
|
|
|
|
/// - 单条 `status_post` 序列化后约 **1.1~1.2KB**(含 36 通道 18 字符 hex + 柜级字段);
|
|
|
|
|
|
/// - 68 柜规模:每柜每天 1440 条(协议 §5.3 固定 60 秒一次)⇒ 约 **9.8 万条/天 ≈ 115MB/天**,
|
|
|
|
|
|
/// 20 万条可容纳 **约 2 天**的完全积压(远超"重启窗口 + 正常积压");
|
|
|
|
|
|
/// - 1000 柜规模:约 **144 万条/天**,20 万条 ≈ **3.3 小时** 的缓冲;
|
|
|
|
|
|
/// - 内存占用上限 ≈ 20 万 × 1.15KB ≈ **230MB**,只占 1GB 实例的约 23%,
|
|
|
|
|
|
/// 给同实例其他业务留足余量(这正是设上限的目的)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ⚠️ 不改队列名、不改报文格式(协议冻结)。
|
|
|
|
|
|
const REPORTS_QUEUE_MAX: i64 = 200_000;
|
|
|
|
|
|
|
2026-09-19 23:40:46 +08:00
|
|
|
|
/// 分发消息:补写设备身份后转发到 Redis 上报队列。
|
2026-07-02 05:38:01 +08:00
|
|
|
|
///
|
2026-09-19 23:40:46 +08:00
|
|
|
|
/// `current_dev_id` = 本 TCP 连接绑定的设备身份(由 `auth_str`/`login` 建立)。
|
|
|
|
|
|
/// 为什么需要它:新协议里业务上行**不带 `dev_id`**(设备身份由连接确定),
|
|
|
|
|
|
/// 而 api-server 依赖 `dev_id` 识别设备,故转发前必须补上。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 本服务**不直接回复设备**:应答由 api-server 经 Redis `device:commands` 下发,
|
|
|
|
|
|
/// 因此这里没有返回值(历史上曾返回 `Option<ServerResponse>` 但恒为 `None`,
|
|
|
|
|
|
/// 那个结构体副本已删除,避免与 api-server 的真实定义漂移)。
|
2026-07-02 05:38:01 +08:00
|
|
|
|
pub async fn handle_message(
|
2026-09-19 23:40:46 +08:00
|
|
|
|
mut msg: DeviceMessage,
|
|
|
|
|
|
current_dev_id: Option<&str>,
|
2026-07-02 05:38:01 +08:00
|
|
|
|
_pool: &ConnectionPool,
|
|
|
|
|
|
redis: &redis::aio::ConnectionManager,
|
2026-09-19 23:40:46 +08:00
|
|
|
|
) {
|
|
|
|
|
|
attach_dev_id(&mut msg, current_dev_id);
|
|
|
|
|
|
|
2026-07-02 05:38:01 +08:00
|
|
|
|
// 所有消息统一转发到 Redis,由 API 服务的 report_worker 处理
|
|
|
|
|
|
forward_to_redis(redis, &msg).await;
|
|
|
|
|
|
|
|
|
|
|
|
// 不直接回复设备,等待 API 服务通过 device:commands 队列回复
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 23:40:46 +08:00
|
|
|
|
/// 把连接绑定的设备身份写入待转发报文。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 以**连接**为准(而非报文自带值):设备身份由 TCP 连接确定,
|
|
|
|
|
|
/// 这样即使设备漏带/错带 `dev_id`,api-server 侧也不会认错设备。
|
|
|
|
|
|
fn attach_dev_id(msg: &mut DeviceMessage, current_dev_id: Option<&str>) {
|
|
|
|
|
|
match current_dev_id {
|
|
|
|
|
|
Some(dev_id) if !dev_id.is_empty() => msg.dev_id = Some(dev_id.to_string()),
|
|
|
|
|
|
// 理论上不会出现:未登录连接的业务报文已被 server.rs 的门控丢弃。
|
|
|
|
|
|
// 此时仍照常转发(不丢数据),仅记 WARN 便于排查。
|
|
|
|
|
|
_ => tracing::warn!(
|
|
|
|
|
|
"[forward] 连接未绑定 dev_id,转发报文将不含设备身份, act={}",
|
|
|
|
|
|
msg.act
|
|
|
|
|
|
),
|
2026-07-02 05:38:01 +08:00
|
|
|
|
}
|
2026-09-19 23:40:46 +08:00
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
|
2026-09-19 23:40:46 +08:00
|
|
|
|
/// 将设备消息转发到 Redis 统一上报队列 `device:reports`
|
2026-09-20 01:21:17 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// `LPUSH` 后用同一条 pipeline 做 `LLEN → LTRIM → LLEN`:
|
|
|
|
|
|
/// - 保证队列**有界**(上限见 [`REPORTS_QUEUE_MAX`]);
|
|
|
|
|
|
/// - 裁剪发生时记 **WARN** 并给出被丢弃条数 —— 运维必须知道"丢了上报",
|
|
|
|
|
|
/// 而不是让 Redis 在无界增长里悄悄 OOM。
|
2026-09-19 23:40:46 +08:00
|
|
|
|
async fn forward_to_redis(redis: &redis::aio::ConnectionManager, msg: &DeviceMessage) {
|
|
|
|
|
|
let json_str = match serde_json::to_string(msg) {
|
|
|
|
|
|
Ok(json) => json,
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
// 序列化失败说明报文结构异常,记 ERROR 并丢弃(不推送半截数据)
|
|
|
|
|
|
tracing::error!("[forward] 消息序列化失败 act={} err={}", msg.act, e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-02 05:38:01 +08:00
|
|
|
|
|
2026-09-19 23:40:46 +08:00
|
|
|
|
let mut conn = redis.clone();
|
2026-09-20 01:21:17 +08:00
|
|
|
|
// 一条 pipeline 完成:入队 → 取裁剪前长度 → 裁剪到上限 → 取裁剪后长度
|
|
|
|
|
|
// (4 条命令 1 次往返;`ignore()` 的两条不占返回位)
|
|
|
|
|
|
let mut pipe = redis::pipe();
|
|
|
|
|
|
pipe.cmd("LPUSH").arg("device:reports").arg(&json_str).ignore();
|
|
|
|
|
|
pipe.cmd("LLEN").arg("device:reports");
|
|
|
|
|
|
pipe.cmd("LTRIM")
|
2026-07-02 05:38:01 +08:00
|
|
|
|
.arg("device:reports")
|
2026-09-20 01:21:17 +08:00
|
|
|
|
.arg(0)
|
|
|
|
|
|
.arg(REPORTS_QUEUE_MAX - 1)
|
|
|
|
|
|
.ignore();
|
|
|
|
|
|
pipe.cmd("LLEN").arg("device:reports");
|
|
|
|
|
|
|
|
|
|
|
|
let result: Result<(i64, i64), redis::RedisError> = pipe.query_async(&mut conn).await;
|
2026-07-02 05:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
match result {
|
2026-09-20 01:21:17 +08:00
|
|
|
|
Ok((len_before_trim, len_after_trim)) => {
|
|
|
|
|
|
if len_before_trim > len_after_trim {
|
|
|
|
|
|
// 越界裁剪:明确告知丢弃了多少条(这些上报已不可恢复,需要人工关注消费端状态)
|
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"[forward] device:reports 超过上限 {},已裁剪最旧的 {} 条上报(长度 {} → {})",
|
|
|
|
|
|
REPORTS_QUEUE_MAX,
|
|
|
|
|
|
len_before_trim - len_after_trim,
|
|
|
|
|
|
len_before_trim,
|
|
|
|
|
|
len_after_trim
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"[forward] 已推送 dev_id={} act={} queue_len={}",
|
|
|
|
|
|
msg.dev_id.as_deref().unwrap_or("?"),
|
|
|
|
|
|
msg.act,
|
|
|
|
|
|
len_after_trim
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
}
|
2026-09-20 01:21:17 +08:00
|
|
|
|
Err(e) => tracing::error!("[forward] Redis LPUSH/LTRIM 失败: {}", e),
|
2026-07-02 05:38:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|