charging-cabinet/tasks/016-tcp-service-refactor.md

325 lines
11 KiB
Markdown
Raw Normal View History

# 任务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服务不处理业务逻辑只负责通信和解析