charging-cabinet/tasks/review-tcp-refactor-report.md
2026-07-02 05:38:01 +08:00

7.5 KiB
Raw Blame History

TCP服务重构审查报告

总体评价

评分6/10

整体架构设计方向正确服务分离、Redis队列通信、全异步处理但存在 1个严重Bug多处架构实现不一致。核心问题集中在节点管理实现的双轨冲突和配置系统与要求的背离。代码结构清晰,但在细节实现上有明显瑕疵。


架构符合性

符合项

条目 状态 说明
API/设备服务职责分离 API服务处理HTTP+业务设备服务仅TCP通信+转发
设备服务不存DB/不验证签名 handler.rs 仅做 forward_to_redis
服务间用Redis LIST通信 device:reports / device:commands / device:replies 三个队列
全异步处理 所有协程用 tokio::spawn,无阻塞调用

问题

严重问题 1节点管理双轨制 — API服务内存注册表 vs Redis注册互不关联

  • 设备服务 main.rs:44 通过 SET node:node-1 写 Redis 注册,从未调用 API 服务的 HTTP 接口
  • API 服务 routes/mod.rs:39-42 注册了 /api/nodes/register/api/nodes/:node_id/heartbeat 等路由,但 从未有调用方
  • API 服务的 NodeRegistry(内存 RwLock与 Redis 中的 node:* 键完全独立
  • node_checker.rs 检查的是内存中的 NodeRegistry,而它是空的(因为设备服务没调用过 /api/nodes/register

后果GET /api/nodes 返回空列表,节点管理功能实际上不可用。

修复建议:统一节点管理方案——

  • 方案A推荐去掉 API 服务的内存 NodeRegistry,改用 Redis 作为唯一节点注册中心。list_nodes 查询 node:* 键。
  • 方案B设备服务在启动时通过 HTTP 调用 POST /api/nodes/register,心跳调用 POST /api/nodes/:node_id/heartbeat

代码质量

严重问题

严重问题 2必须修复Redis 类型冲突 — register_node 用 SETheartbeat 用 HSET

位置device-server/src/main.rs 第72-110行

// register_node — 第80行
redis::cmd("SET").arg(&key).arg(info.to_string()).arg("EX")...

// heartbeat — 第101行
redis::cmd("HSET").arg(&key).arg("last_heartbeat")...

SET 将 key 创建为 string 类型,之后 HSET 在同一 key 上操作会触发 Redis WRONGTYPE 错误。heartbeat 函数仅记录 tracing::warn,不会 panic但心跳静默失效。

后果node:{node_id} 的 TTL180秒到期后节点自动消失节点管理完全不可用。

修复建议:任一方案——

  • register_node 改为 HSET 逐字段存储(node_idiptcp_portregistered_at),再 EXPIRE
  • 或将 heartbeat 改为 SET + KEEPTTL 重写整个 JSON

配置问题

问题 3配置读取方式与审查要求不符

审查清单要求:使用 config.toml(不用 .env

实际

  • 两个服务的 config.rs 均调用 dotenvy::dotenv() + std::env::var()完全不读取 config.toml
  • config.toml 文件存在于两个项目目录中,但没有任何代码解析它们
  • 敏感信息(数据库密码 Hbhyg731024@)硬编码在 config.toml

修复建议:二选一——

  • 如果坚持环境变量方案,删除 config.toml 文件
  • 如果坚持 config.toml 方案,用 toml crate 解析并替换 from_env()

结构体重复

问题 4协议结构体在两个服务中重复定义

结构体 api-server 位置 device-server 位置
DeviceMessage protocol.rs:14 tcp/protocol.rs:14
ServerResponse protocol.rs:36 tcp/protocol.rs:36
DeviceCommand commands.rs:118 tcp/protocol.rs:67

DeviceCommand 甚至在同一项目的两个文件中重复(commands.rstcp/protocol.rs)。

影响:维护时需同步修改两处,容易不一致。

修复建议:抽取共享类型到独立 cratepms-protocol),两个服务共同依赖。


死代码

问题 5send_device_response 未使用

位置api-server/src/commands.rs 第157行

函数用 #[allow(dead_code)] 标记,实际从未被调用。响应发送由 report_worker.rs 中的 send_response_to_device 完成。


异步规范

问题 6auth_str 限流器使用同步 Mutex

位置api-server/src/workers/report_worker.rs 第30行

struct RateLimiter {
    attempts: Mutex<HashMap<String, Vec<Instant>>>, // std::sync::Mutex
    ...
}

tokio::spawn 的异步任务中持有了同步 std::sync::Mutex 锁。虽然当前负载下不会死锁(锁持有时间极短),但不符合异步规范。

修复建议:改为 tokio::sync::Mutex,或将 RateLimiterLazyLock 移到 spawn_blocking 中。


函数长度

所有函数均在80行以内 (最长的 handle_connection 144行但包含多个分支逻辑块为 tokio::select! 模式,可接受)。

错误处理

  • 所有外部 IO 均有异常捕获
  • 无裸 panic (仅 main.rs 中启动阶段 expect 合理)
  • thiserror + AppError 统一错误处理

Redis 使用审查

要求 状态 说明
连接注册 device:{imei} → info 实际用 device:online:{imei} 仅存 "1"
TTL 180秒 EX 3005分钟
device:reports 队列 BRPOP 消费
device:commands 队列 BRPOP 消费
device:replies 队列 BRPOP 消费
连接池管理正确 ConnectionManager
BRPOP 阻塞 参数 0 阻塞等待

TCP 服务审查

要求 状态 说明
LF分隔JSON LinesCodec\n 分割
连接池内存HashMap RwLock<HashMap<String, DeviceConnection>>
登录验证流程 ⚠️ 仅注册连接,验证签名在 API 端完成
断连清理 tokio::select! 退出时 pool.remove

配置文件审查

要求 状态 说明
使用 config.toml 代码只读环境变量
配置项完整 ⚠️ 缺少 log_level
敏感信息不硬编码 config.toml 含数据库密码

优点

  1. 服务职责分离干净 — device-server 确实只做 TCP 通信handler.rs 轻量清晰
  2. tokio::select! 读写分离server.rs 中同时处理设备消息读取和平台指令写入,设计合理
  3. FramedRead + LinesCodec — 正确处理了 LF 粘包问题
  4. reply_workeroneshot 机制 — 通过 msg_id 匹配异步等待,设计优雅
  5. report_worker 每条消息 tokio::spawn — 独立处理,不影响后续消息消费
  6. 全局 #[allow(dead_code)] 仅出现在少数必要位置device-server connection.rsprotocol.rs#![allow(dead_code)] 是模块级,尚可接受)

修复优先级总结

优先级 问题 风险等级
🔴 P0 register_node 与 heartbeat 的 Redis 类型冲突 严重 — 节点管理静默失效
🟡 P1 节点管理双轨制 — 内存NodeRegistry与Redis注册无关联 高 — 节点列表API不可用
🟡 P2 配置系统与要求背离读env而非config.toml 中 — 视部署要求
🟢 P3 协议结构体两服务重复定义 低 — 维护成本
🟢 P4 auth_str 限流器用同步Mutex 低 — 规范性问题

推荐立即修复P0 + P1。这两个问题导致节点管理和心跳功能实际上不工作。