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

11 KiB
Raw Blame 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

[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

[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

// 设备服务收到设备消息后推送到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→设备

// 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

// 设备服务收到设备响应后推送到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服务

节点注册:

// 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})))
}

节点心跳:

// 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})))
}

节点注销:

// 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})))
}

节点列表:

// GET /api/nodes
async fn list_nodes() -> Result<Json<Vec<NodeInfo>>> {
    Ok(Json(json!(nodes.values().collect::<Vec<_>>())))
}

5. 设备服务注册流程

// 设备服务启动时
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匹配

// 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. 自动充电流程

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