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

197 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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行
```rust
// 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_id``ip``tcp_port``registered_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.rs``tcp/protocol.rs`)。
**影响**:维护时需同步修改两处,容易不一致。
**修复建议**:抽取共享类型到独立 crate`pms-protocol`),两个服务共同依赖。
---
### 死代码
**问题 5`send_device_response` 未使用**
**位置**`api-server/src/commands.rs` 第157行
函数用 `#[allow(dead_code)]` 标记,实际从未被调用。响应发送由 `report_worker.rs` 中的 `send_response_to_device` 完成。
---
### 异步规范
**问题 6`auth_str` 限流器使用同步 Mutex**
**位置**`api-server/src/workers/report_worker.rs` 第30行
```rust
struct RateLimiter {
attempts: Mutex<HashMap<String, Vec<Instant>>>, // std::sync::Mutex
...
}
```
`tokio::spawn` 的异步任务中持有了同步 `std::sync::Mutex` 锁。虽然当前负载下不会死锁(锁持有时间极短),但不符合异步规范。
**修复建议**:改为 `tokio::sync::Mutex`,或将 `RateLimiter``LazyLock` 移到 `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 300`5分钟 |
| `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_worker``oneshot` 机制** — 通过 msg_id 匹配异步等待,设计优雅
5. **`report_worker` 每条消息 `tokio::spawn`** — 独立处理,不影响后续消息消费
6. **全局 `#[allow(dead_code)]` 仅出现在少数必要位置**device-server `connection.rs``protocol.rs``#![allow(dead_code)]` 是模块级,尚可接受)
---
## 修复优先级总结
| 优先级 | 问题 | 风险等级 |
|--------|------|---------|
| 🔴 P0 | register_node 与 heartbeat 的 Redis 类型冲突 | 严重 — 节点管理静默失效 |
| 🟡 P1 | 节点管理双轨制 — 内存NodeRegistry与Redis注册无关联 | 高 — 节点列表API不可用 |
| 🟡 P2 | 配置系统与要求背离读env而非config.toml | 中 — 视部署要求 |
| 🟢 P3 | 协议结构体两服务重复定义 | 低 — 维护成本 |
| 🟢 P4 | auth_str 限流器用同步Mutex | 低 — 规范性问题 |
**推荐立即修复**P0 + P1。这两个问题导致节点管理和心跳功能实际上不工作。