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

325 lines
11 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.

# 任务016TCP服务重构
## 目标
重构TCP服务拆分为API服务和设备服务使用Redis做服务发现和消息队列。
## 架构设计
```
┌─────────────────────────────────────────────────────────────┐
│ API服务 (api-server) — 主服务 │
│ - HTTP API后台管理 + H5端口: 3000 │
│ - 前端轮询刷新每5秒
│ - 业务逻辑处理(验证/存DB/告警) │
│ - 节点管理(设备服务注册/心跳) │
│ - 后台协程处理Redis队列 │
└─────────────────────────────────────────────────────────────┘
↑ Redis LIST ↑ Redis LIST
│ device:report:{imei} │ device:cmd:{imei}
│ (设备上报) │ (平台指令)
│ │
┌─────────────────────────────────────────────────────────────┐
│ 设备服务 (device-server) — 轻量化 │
│ - TCP监听: 3002设备长连接
│ - 只处理通信和指令解析 │
│ - 不存DB、不验证签名、不处理业务 │
│ - 注册到API服务启动/心跳/注销) │
│ - Redis连接注册: device:{imei} → {node_id, ip, port} │
└─────────────────────────────────────────────────────────────┘
```
## 核心设计原则
1. **TCP服务轻量化** — 只处理TCP通信和指令解析不处理业务逻辑
2. **Redis做缓冲** — 服务间通信用Redis LIST不用HTTP直接调用
3. **全异步处理** — 所有指令异步处理msg_id匹配响应
4. **协程处理任务** — API服务用tokio::spawn处理Redis队列消息
## 重构清单
### 1. 项目结构拆分
**当前:** 单服务 `software/server/`
**重构后:**
```
software/
├── api-server/ # API服务主服务
│ ├── Cargo.toml
│ ├── config.toml
│ └── src/
│ ├── main.rs
│ ├── config.rs
│ ├── routes/
│ │ ├── devices.rs # 设备管理API
│ │ ├── nodes.rs # 节点管理API
│ │ └── ...
│ ├── workers/ # 后台协程
│ │ ├── report_worker.rs # 处理设备上报
│ │ └── reply_worker.rs # 处理设备响应
│ ├── middleware/
│ ── h5/
├── device-server/ # 设备服务(轻量化)
│ ├── Cargo.toml
│ ├── config.toml
│ └── src/
│ ├── main.rs
│ ├── config.rs
│ ├── tcp/
│ │ ├── server.rs # TCP监听
│ │ ├── connection.rs # 连接管理内存HashMap
│ │ ├── protocol.rs # 协议解析
│ │ └── handler.rs # 消息处理(解析+转发Redis
│ └── redis.rs # Redis连接注册
```
**说明:** 暂不提取shared crate协议定义等代码先复制到两个服务。
### 2. 配置文件config.toml
**api-server/config.toml**
```toml
[server]
host = "0.0.0.0"
port = 3000
[database]
url = "mysql://root:password@10.8.0.252:3306/pms_dev"
[redis]
url = "redis://:password@10.8.0.252:6379/5"
[jwt]
secret = "pms-dev-secret-change-me-in-production"
[export]
dir = "./exports"
[node]
heartbeat_timeout = 180
```
**device-server/config.toml**
```toml
[server]
host = "0.0.0.0"
tcp_port = 3002
node_id = "node-1"
[redis]
url = "redis://:password@10.8.0.252:6379/5"
[api_server]
url = "http://127.0.0.1:3000"
heartbeat_interval = 60
[device]
register_ttl = 180
```
### 3. 服务间通信Redis LIST
**设备上报设备→API**
```rust
// 设备服务收到设备消息后推送到Redis
async fn forward_to_api(redis: &Redis, imei: &str, msg: &DeviceMessage) {
redis.lpush(&format!("device:report:{}", imei), serde_json::to_string(msg)).await;
}
// API服务后台协程处理上报
async fn process_reports(redis: Redis, mysql: MySqlPool) {
loop {
let (_, msg_json) = redis.brpop(&["device:report:*"], 0).await;
let msg: DeviceMessage = serde_json::from_str(&msg_json).unwrap();
tokio::spawn(handle_report(msg, mysql.clone())); // 异步处理
}
}
```
**平台指令API→设备**
```rust
// API服务下发指令推送到Redis
async fn send_command(redis: &Redis, imei: &str, cmd: DeviceCommand) {
redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await;
}
// 设备服务:阻塞弹出指令,发送到设备
async fn process_commands(redis: Redis, pool: ConnectionPool) {
loop {
let (key, cmd_json) = redis.brpop(&["device:cmd:*"], 0).await;
let imei = extract_imei(&key);
let cmd: DeviceCommand = serde_json::from_str(&cmd_json).unwrap();
// 发送到设备(异步等待响应)
if let Some(conn) = pool.get(&imei) {
conn.send(cmd).await;
}
}
}
```
**指令响应设备→API**
```rust
// 设备服务收到设备响应后推送到Redis
async fn forward_reply(redis: &Redis, imei: &str, reply: DeviceResponse) {
redis.lpush(&format!("device:reply:{}", imei), serde_json::to_string(&reply)).await;
}
// API服务后台协程匹配msg_id
async fn process_replies(redis: Redis, pending: PendingCommands) {
loop {
let (_, reply_json) = redis.brpop(&["device:reply:*"], 0).await;
let reply: DeviceResponse = serde_json::from_str(&reply_json).unwrap();
if let Some(tx) = pending.remove(&reply.msg_id) {
tx.send(reply).ok();
}
}
}
```
### 4. 节点管理API服务
**节点注册:**
```rust
// POST /api/nodes/register
async fn register_node(Json(req): Json<NodeRegisterRequest>) -> Result<Json<Value>> {
// 记录节点信息到内存/DB
nodes.insert(req.node_id.clone(), NodeInfo {
id: req.node_id,
ip: req.ip,
tcp_port: req.tcp_port,
status: "online",
last_heartbeat: Instant::now(),
});
Ok(Json(json!({"suc": 1})))
}
```
**节点心跳:**
```rust
// POST /api/nodes/:node_id/heartbeat
async fn node_heartbeat(Path(node_id): Path<String>) -> Result<Json<Value>> {
if let Some(node) = nodes.get_mut(&node_id) {
node.last_heartbeat = Instant::now();
}
Ok(Json(json!({"suc": 1})))
}
```
**节点注销:**
```rust
// POST /api/nodes/:node_id/deregister
async fn deregister_node(Path(node_id): Path<String>) -> Result<Json<Value>> {
nodes.remove(&node_id);
Ok(Json(json!({"suc": 1})))
}
```
**节点列表:**
```rust
// GET /api/nodes
async fn list_nodes() -> Result<Json<Vec<NodeInfo>>> {
Ok(Json(json!(nodes.values().collect::<Vec<_>>())))
}
```
### 5. 设备服务注册流程
```rust
// 设备服务启动时
async fn startup(api_url: &str, node_id: &str, ip: &str, tcp_port: u16) {
// 1. 注册到API服务
let client = reqwest::Client::new();
client.post(&format!("{}/api/nodes/register", api_url))
.json(&json!({ "node_id": node_id, "ip": ip, "tcp_port": tcp_port }))
.send().await;
// 2. 启动心跳协程每60秒
let api_url = api_url.to_string();
let node_id = node_id.to_string();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
client.post(&format!("{}/api/nodes/{}/heartbeat", api_url, node_id))
.send().await;
}
});
}
// 设备服务关闭时
async fn shutdown(api_url: &str, node_id: &str) {
let client = reqwest::Client::new();
client.post(&format!("{}/api/nodes/{}/deregister", api_url, node_id))
.send().await;
}
```
### 6. 异步指令处理msg_id匹配
```rust
// API服务下发指令
async fn send_command_to_device(imei: &str, cmd: DeviceCommand) -> Result<DeviceResponse> {
let (tx, rx) = oneshot::channel();
pending_commands.insert(cmd.msg_id, tx);
// 推送到Redis队列
redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await;
// 等待响应超时60秒
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => Ok(response),
Ok(Err(_)) => Err(AppError::Internal("通道关闭".into())),
Err(_) => {
pending_commands.remove(&cmd.msg_id);
Err(AppError::Timeout("设备响应超时".into()))
}
}
}
```
### 7. 自动充电流程
```rust
// API服务收到 bat_in 上报
async fn handle_bat_in(imei: &str, channel: &str, bms: &BmsData) {
// 1. 检查通道是否已有电池
if let Some(old_battery) = battery_map.get(channel) {
// 旧电池强制结束
record_charge_complete(channel, old_battery, ChargeEndReason::Forced);
}
// 2. 记录新电池
battery_map.insert(channel.to_string(), battery.clone());
// 3. 验证电池合规性
match validate_battery(bms) {
Ok(()) => {
// 4. 自动下发充电指令
send_command_to_device(imei, DeviceCommand {
act: "on".to_string(),
id: channel.to_string(),
msg_id: generate_msg_id(),
}).await;
}
Err(e) => {
// 验证失败,记录日志
tracing::warn!("电池验证失败: {}", e);
}
}
}
```
## 质量约束
1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告
2. 分层拆分单函数≤80行命名语义化完整注释
3. 所有外部IO/网络请求异常捕获禁止裸panic
4. 分支逻辑全覆盖,不遗漏兜底分支
5. 常量抽离不使用废弃API
6. Rust内存安全合理管理所有权禁用unsafe无合理理由
7. 异步代码无阻塞操作
8. 指令处理必须异步msg_id匹配响应
9. TCP服务不处理业务逻辑只负责通信和解析