12451 999885c38f feat(protocol): TCP v1.1 协议对齐 + 平台侧兜底 + 生产部署修复
协议(唯一权威文档:docs/柜控板固件对接文档.md)
- 应答必带 act(与请求一致)、msg_id 与请求一致;判"应答 vs 指令"只看有没有 suc
- dev_id 只在 auth_str/login 携带;其余报文用内部字段 _dev_id 路由(下发前由 device-server 摘除)
- id 只表示动作目标(柜级省略 / 一段=单板 / 两段=仓体级),不再含 IMEI
- msg_id 每连接从 1 起自增;60 秒交互超时;status_post 每通道 18 字符 hex 全量上报
- 未登录连接 30 秒内未 login 即断开;未登录只放行 auth_str/login

平台侧实现
- 上行解析按新结构重写(cabinet/boards/battery),删除废弃的 status/parsed_status
- 订单:唯一入口 = on;bat_in 只登记电池台账;4 条结束路径统一走 finalize_order(含充电记录落库)
- 新增设备日志保留策略(90 天,分批删除);未登记 IMEI 静默丢弃;停电标记按 status1 bit0 恢复清除
- 待确认第 13 项兜底:平台侧按 电压 × 电流 × 时间 积分计算订单电量(workers/energy.rs)
- 待确认第 18 项兜底:status_post bit7 连续 2 次无电池 → 判定电池被拔走并提前结单

修复(均为部署后才暴露的问题)
- 队列消费禁用 BRPOP:无限阻塞会因代理掐连接永久卡死(零错误日志);有限超时也只是
  "超时才醒",下行指令延迟 15~45 秒 → 改为 RPOP 非阻塞轮询,延迟降到 0.1~0.2 秒
- sync_iccid 把可空列 iccid 按 Option<String> 解码 → 整条 login 失败(ICCID 未上报的新柜子永远登录不上)
- 迁移读 information_schema.COLUMN_COMMENT 需 CAST(... AS CHAR)(MySQL 8 驱动报 VARBINARY,否则迁移 panic)
- 部署注意事项:.env 不支持行内注释;pscp 上传需 chmod +x;运行中二进制需 .new + mv 原子替换

部署
- 生产迁到 API 9000 / TCP 9001(systemd 单元补 EnvironmentFile、nginx 同步指向)
- 新增 deploy/deploy-prod.ps1(本地一键:构建→校验→备份→原子替换→重启→验收→无副作用探测)
- 废弃 deploy-prod.sh / deploy-test.sh(原脚本会在 target/release 找产物、装重复 systemd 单元、
  并用本地 .env 覆盖线上 .env)

质量
- 单测:api-server 39 个 / device-server 13 个(含协议示例逐值断言、跨模块 Redis key 契约)
- 两服务 cargo check 零错误零告警;web tsc --noEmit 通过
- test/tcp_e2e_smoke.py 端到端联调 13/13 通过
2026-09-19 23:41:09 +08:00

27 KiB
Raw Blame History

API 接口文档

充电柜矩阵系统(PMS)接口规范。PC 管理端与微信小程序用户端共用同一套 API。

基础信息

  • 测试环境:https://test.anzhizhichong.com/pms/api/
  • 正式环境:https://app.anzhizhichong.com/pms/api/
  • 认证方式:Bearer Token(JWT)
  • 响应格式:{ "code": 0, "data": ..., "message": "ok" }

统一端点清单

方法 路径 说明 权限
POST /api/auth/login 登录 免认证
GET /api/auth/logout 登出 需认证
GET /api/auth/me 当前用户信息 需认证
GET /api/dashboard 仪表盘数据 需认证
GET /api/cabinets 设备列表(含 RSSI) device:view
POST /api/cabinets 批量创建柜子 device:create
GET /api/cabinets/:id 设备详情(含仓板+仓体) device:view
PUT /api/cabinets/:id 更新柜子 device:edit
DELETE /api/cabinets/:id 删除柜子 device:delete
POST /api/cabinets/:id/regenerate-auth 重置安全码(仅总管理员) device:auth_str:reset
POST /api/cabinets/:id/bind 绑定设备到项目 device:edit
POST /api/cabinets/:id/replace-imei 更换 IMEI device:edit
GET /api/cabinets/abstract/:abstract_id 扫码查设备(免认证) 免认证
GET /api/cabinets/scan/:abstract_id 扫码验证(需认证) 需认证
GET /api/cabinets/filter 设备列表(管理端) 需认证
GET /api/cabinets/options 设备下拉选项 需认证
GET /api/cabin-boards/options 仓控板下拉选项 需认证
GET /api/compartments/options 仓体下拉选项 需认证
GET /api/cabinets/realtime-status 设备实时状态 需认证
GET /api/compartments/:id 仓体/通道详情 需认证
POST /api/charge/start 开始充电 charge:start
POST /api/charge/stop 停止充电 charge:stop
POST /api/door/open 开门(柜级:整柜共用一扇门,只解锁需人拉开) charge:open_door
POST /api/cabinets/{id}/open 解锁柜门(柜级,同上;管理端设备详情页用) charge:open_door
POST /api/cabinets/{id}/pow-on 接触器合闸(柜级) device:operate
POST /api/cabinets/{id}/pow-off 接触器分闸(柜级) device:operate
POST /api/cabinets/{id}/reset 重启整柜(柜级) device:operate
POST /api/cabin-boards/{id}/reset 重启单块仓控板 device:operate
GET /api/organizations 组织列表 org:view
POST /api/organizations 创建组织 org:create
PUT /api/organizations/:id 更新组织 org:edit
DELETE /api/organizations/:id 删除组织 org:delete
GET /api/organizations/:org_id/projects 项目列表 需认证
POST /api/organizations/:org_id/projects 创建项目 project:create
PUT /api/projects/:id 更新项目 project:edit
DELETE /api/projects/:id 删除项目 project:delete
GET /api/organization-tree 组织树(含项目/设备) 需认证
GET /api/charge-records 充电记录 charge_record:view
GET /api/charge-records/export 导出充电记录 charge_record:export
GET /api/charge-orders 充电订单列表 charge_record:view
GET /api/batteries 电池台账列表 device:view
GET /api/energy-stats/summary 能耗概览 energy:view
GET /api/energy-stats/cabinets 柜子能耗排行 energy:view
GET /api/energy-stats/projects 项目能耗统计 energy:view
GET /api/energy-stats/trend 能耗趋势 energy:view
GET /api/energy-stats/export 导出能耗数据 energy:view
GET /api/device-logs 设备日志 device:view
GET /api/operation-logs 操作日志 system:log
GET /api/users 用户列表 user:view
POST /api/users 创建用户 user:create
PUT /api/users/:id 更新用户 user:edit
DELETE /api/users/:id 删除用户 user:delete
PUT /api/users/:id/reset-password 重置密码 user:edit
PUT /api/users/:id/status 设置用户状态 user:edit
GET /api/users/:id/permissions 用户权限 user:view
PUT /api/users/:id/roles 分配角色 user:edit
GET /api/users/:id/scopes 用户数据范围 user:view
PUT /api/users/:id/scopes 设置数据范围 user:edit
GET /api/roles 角色列表 role:view
POST /api/roles 创建角色 role:create
PUT /api/roles/:id 更新角色 role:edit
DELETE /api/roles/:id 删除角色 role:delete
PUT /api/roles/:id/permissions 角色赋权 role:edit
GET /api/permissions 权限码列表 role:view
GET /api/downloads 下载中心 需认证
GET /api/downloads/:id 下载详情 需认证
GET /api/downloads/:id/file 下载文件 需认证
POST /api/downloads/cleanup 清理过期下载 需认证
POST /api/devices/:dev_id/command 下发设备指令 device:operate
GET /api/devices/stats 设备服务统计 需认证
GET /api/health 健康检查 免认证

认证接口

POST /api/auth/login

请求:

{
  "phone": "13800138000",
  "password": "admin123"
}

响应:

{
  "code": 0,
  "data": {
    "token": "eyJ0eXAi...",
    "user": {
      "id": 1,
      "name": "管理员",
      "phone": "13800138000",
      "role_level": 2,
      "organization_id": null,
      "permissions": ["device:view", "device:create", "..."]
    }
  },
  "message": "登录成功"
}

角色等级: 0=操作员,1=企业管理员,2=总管理员


小程序专用接口

GET /api/dashboard

小程序首页仪表盘数据。支持 org_id 参数按组织过滤。

响应:

{
  "code": 0,
  "data": {
    "online_cabinets": 68,
    "charging_count": 214,
    "idle_channels": 722,
    "fault_channels": 0,
    "full_count": 0,
    "today_charge_count": 0,
    "today_energy_kwh": 0,
    "today_charge_hours": 0,
    "alerts": [],
    "projects": [
      { "id": 1, "name": "无锡太湖新城换电站", "cabinet_count": 2, "charging": 16, "idle": 56, "fault": 0 }
    ]
  }
}

GET /api/cabinets

设备列表,支持 project_id 和 org_id 过滤。响应包含实时 RSSI。

响应字段:

{
  "id": 1,
  "abstract_id": "10-00000001",
  "name": "太湖新城换电柜A",
  "status": 1,
  "project_id": 1,
  "rssi": 85,
  "channel_count": 36,
  "idle_count": 30,
  "charging_count": 6,
  "full_count": 0,
  "fault_count": 0
}

GET /api/cabinets/abstract/:abstract_id

扫码入口,免认证。通过二维码中的抽象ID查找设备。

响应:

{
  "code": 0,
  "data": {
    "id": 1,
    "abstract_id": "10-00000001",
    "imei": "864720562557021",
    "iccid": "8986012520983901841",
    "name": "太湖新城换电柜A",
    "status": 1,
    "cabin_boards": [
      {
        "id": 1,
        "board_id": 1,
        "status": 0,
        "compartments": [
          { "id": 1, "channel_id": 1, "status": 0 }
        ]
      }
    ]
  }
}

GET /api/compartments/:id

仓体/通道详情,含实时 BMS 数据和充电记录。

响应字段:

  • voltage — mV(整数)
  • current — mA(整数)
  • energy — Wh(整数)
  • soc — ‰(0-10000,前端已自动转为 %)
  • cell_voltages — mV 数组

POST /api/charge/start

请求: { "compartment_id": 123 }

权限:charge:start + 组织隔离(用户只能操作所属组织的设备)

POST /api/charge/stop

请求: { "compartment_id": 123 }

权限:charge:stop + 组织隔离

POST /api/door/open

请求: { "compartment_id": 123 }

权限:charge:open_door + 组织隔离

GET /api/cabinets/scan/:abstract_id

扫码验证,需认证。验证当前用户是否有权访问该设备。


管理端接口

GET /api/charge-records

充电记录列表,支持按柜子/时间过滤,响应含组织数据隔离。

查询参数:

参数 类型 说明
page int 页码(默认 1)
page_size int 每页条数(默认 20,最大 100)
cabinet_id int 按柜子过滤
cabin_board_id int 按仓控板过滤
compartment_id int 按仓体过滤
start_time string 开始时间(含),格式 YYYY-MM-DD HH:MM:SS
end_time string 结束时间(含),格式同上

响应:

{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "cabinet_id": 1,
        "cabinet_name": "太湖新城换电柜A",
        "cabin_board_id": 1,
        "board_id": 1,
        "compartment_id": 1,
        "channel_id": 1,
        "start_time": "2026-07-05 10:00:00",
        "end_time": "2026-07-05 12:00:00",
        "energy": 1200,
        "max_temperature": 30435,
        "status": 1,
        "status_text": "正常结束"
      }
    ],
    "total": 50,
    "page": 1,
    "page_size": 20
  },
  "message": "ok"
}

状态码: 0=充电中,1=正常结束,2=异常中断,3=手动停止

GET /api/charge-records/export

异步导出充电记录 Excel。创建导出任务后自行去下载中心取文件。

查询参数: 同 GET /api/charge-records

响应:

{ "code": 0, "data": { "task_id": 1 }, "message": "导出任务已创建" }

GET /api/charge-orders

充电订单列表。订单记录一次充电事件,含电池ID、SOC变化、结束原因。

查询参数:

参数 类型 说明
page int 页码(默认 1)
page_size int 每页条数(默认 20,最大 100)
cabinet_id int 按柜子过滤
compartment_id int 按仓体过滤
battery_id string 电池ID(模糊匹配)
status int 状态过滤
start_time string 开始时间(含)
end_time string 结束时间(含)

响应:

{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "cabinet_id": 1,
        "cabinet_name": "太湖新城换电柜A",
        "compartment_id": 1,
        "battery_id": "BAT001",
        "start_time": "2026-07-05 10:00:00",
        "end_time": "2026-07-05 12:00:00",
        "start_soc": 20,
        "end_soc": 95,
        "energy": 1200,
        "reason": null,
        "reason_text": "-",
        "status": 1,
        "status_text": "正常结束",
        "created_at": "2026-07-05 10:00:05"
      }
    ],
    "total": 50,
    "page": 1,
    "page_size": 20
  },
  "message": "ok"
}

订单状态: 0=充电中,1=正常结束,2=手动停止,3=异常终止,4=拔出结束

GET /api/batteries

电池台账列表。记录每块电池的插入/拔出事件和当前状态。

查询参数:

参数 类型 说明
page int 页码(默认 1)
page_size int 每页条数(默认 20,最大 100)
status int 状态过滤
battery_id string 电池ID(模糊匹配)

响应:

{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "battery_id": "BAT001",
        "brand": "星恒",
        "model": "XH-48V",
        "voltage": 52000,
        "soc": 45,
        "status": 1,
        "cabinet_id": 1,
        "compartment_id": 1,
        "last_seen": "2026-07-05 12:00:00",
        "created_at": "2026-07-01 08:00:00"
      }
    ],
    "total": 50,
    "page": 1,
    "page_size": 20
  },
  "message": "ok"
}

GET /api/energy-stats/summary

能耗概览,按组织数据隔离返回今日/本月能耗数据。

响应:

{
  "code": 0,
  "data": {
    "today_energy": 45000,
    "month_energy": 1200000,
    "estimated_cost": 840.00,
    "cabinet_count": 36,
    "online_count": 34
  },
  "message": "ok"
}
  • today_energy / month_energy — Wh(整数)
  • estimated_cost — 预估费用(float,元),按 COST_PER_WH 单价计算

数据单位规范

核心原则:所有数值字段使用整数存储,禁止浮点类型 (f32/f64/DECIMAL/FLOAT)

单位转换仅在前端: API 始终返回原始单位值,前端负责转换为用户友好单位显示。后端禁止做任何单位换算。

标量字段

字段 类型 单位 前端显示 转换公式
energy / total_energy / today_energy / month_energy / energy_kwh¹ i64 Wh kWh / 1000
voltage / cell_voltages[] i64 mV V / 1000
current i64 mA A / 1000
soc / start_soc / end_soc i64 ‰ % / 10
temp / cell_temps[] / max_temperature i64 K×100 °C / 100 - 273.15
rssi i64 0–99 信号等级 直接显示 不转换(不是 dBm)
cost / estimated_cost i64/f64 元 ¥ 不转换(后端业务计算)
today_charge_count / today_charge_hours i64 计数值 直接显示 不转换

¹ 注意:energy_kwh / today_energy_kwh 为历史遗留命名,实际值仍为 Wh,前端仍需 / 1000。

示例

原始值 含义 前端显示
52000 电压(mV) 52.0 V
10000 电流(mA) 10.0 A
120000 功率(mW) 120.0 W
1000 充电量(Wh) 1.000 kWh
850 SOC(‰) 85.0%
29865 温度(K×100) 25.5°C
85 RSSI 信号等级(0–99) 85
[32456,32500,32300] 单体电压(mV) 32.456V, 32.500V, 32.300V
[29865,29900] 单体温度(K×100) 25.5°C, 25.9°C

TCP 设备通讯协议

⛔ 本节(自本行起至下一个 ## 标题之前)全部内容已废弃,禁止作为实现依据。 唯一权威文档:柜控板固件对接文档.md(v1.1) —— 所有 TCP 报文格式、字段名、 act 清单、签名算法、保活/超时规则、时序与异常处理,一律以该文档为准。

本节保留仅为历史参考。已知与定稿规范的重大冲突(仅列关键几处,避免误读):

  • dev_id 上行全部报文必带 → 只有 auth_str / login 带,其余报文不带(设备身份由 TCP 连接确定)
  • id = IMEI / IMEI-仓控板-通道 → id 不含 IMEI:柜级省略、单板一段 "01"、仓体级两段 "01-3"
  • 空闲 1 分钟 / 充电 15 秒上报 → status_post 固定每 60 秒一次、每次全量
  • login 应答返回 cell → 返回 spec(柜子规格,如 "0606")与 work_mode
  • pow_fail 带 info → 改为 pow_fail_dc / pow_fail_ac 两条独立指令,无附加字段
  • status_post 的 status HEX 字典 / bat_in 的 bms 块 → 改为 cabinet + boards[].channels[](每通道 18 字符 hex 透传) 与 battery 块
  • bat_in 建单 → bat_in 与订单无关,订单只由 on 建立
  • 内部字段已由 id 改为 _dev_id(供 device-server 路由,下发前摘除)

设计原则

MCU(Air780E)资源有限,TCP 协议遵循以下原则:

  1. 响应扁平化:字段直接放在顶层,不包 data/result 等嵌套层
  2. 字段最小化:需要几个字段传几个,不传空值、默认值
  3. JSON 紧凑:去空格回车,不传多余引号和逗号(可用 serde_json::to_string 紧凑模式)
  4. 响应必达:所有上行报文平台都会回复;但设备回执(带 suc)平台不再回包(防误配对)

通信规则(⚠️ 下列旧描述已废弃,见节首说明)

  • TCP 长连接,无需 PING 包 ✅(保活由 status_post 承担)
  • 空闲 1 分钟上报,充电 15 秒上报 ❌ → 固定每 60 秒全量
  • 超时 5000ms ❌ → 每个交互 60 秒超时,超时作废、迟到应答丢弃、不重试
  • JSON 去空格回车省流量,LF 处理粘包 ✅
  • dev_id:设备上报时使用 ❌ → 仅 auth_str / login 携带
  • id:IMEI-仓控板-通道 / IMEI ❌ → 不含 IMEI:柜级省略 / 单板 "01" / 仓体级 "01-3"
  • msg_id:两个方向各自独立计数,都每连接从 1 起自增,重连归零;判"应答 vs 指令"看有无 suc

签名机制

  • 设备首次连接调 auth_str 接口获取8位安全码(仅一次)
  • sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)
  • 时间戳秒级,平台校验±60秒

指令清单

⛔ 以下整段(含本表与其后的报文示例、cell / pow_fail / bms 字段说明)均为废弃的历史内容,请勿据此实现。 现行清单与报文以 柜控板固件对接文档.md §4 / §5 为准。act 共 13 个: auth_str / login / status_post / pow_fail_dc / pow_fail_ac / bat_in / bat_out / on / off / open / pow_on / pow_off / reset (已无 pow_fail,拆成 pow_fail_dc / pow_fail_ac)

动作 方向 说明(⚠️ 历史,见上方说明)
login 设备→平台 登录(含 sign+timestamp+iccid);成功应答返回 spec + work_mode(不再是 cell)
auth_str 设备→平台 获取安全码(首次 / 重置后;失败统一回 invalid signature 之外的两条见权威文档)
status_post 设备→平台 每 60 秒全量;结构为 cabinet + boards[].channels[](每通道 18 字符 hex),不再是 status 字典
pow_fail 设备→平台 已废弃 → 改为 pow_fail_dc / pow_fail_ac(无附加字段)
pow_fail_dc / pow_fail_ac 设备→平台 直流停电 / 交流停电(事件上报)
bat_in 设备→平台 电池插入(battery 块:属性 + info 18hex + cells hex);不建单
bat_out 设备→平台 电池拔出(结束订单 user_stop)
on 双向 开始充电(上行两形态:带 suc=回执 / 不带=上报;建单唯一入口)
off 双向 充电停止(结束订单 normal)
open 平台→设备 柜门解锁(柜级:整柜共用一扇门,需人拉开;无"关门"指令)
pow_on / pow_off 平台→设备 接触器合闸 / 分闸(pow_off 后仓控板随之下电 → 上报 pow_fail_dc)
reset 平台→设备 重启整柜(id 省略)或单块仓控板(id="01");先回执再重启

⚠️ 所有设备上报消息平台都会回复 {"suc":1,"msg_id":N} 确认。响应字段扁平化,不加 data 包裹。

全局单位规范

所有数值字段使用整数存储,禁止浮点类型:

字段 单位 精度 说明
voltage mV 1 52000 = 52.000V
current mA 1 10000 = 10.000A
temp K×100 0.01K 29865 = 298.65K = 25.5°C
soc ‰ 0.1% 4500 = 45.00%

服务架构

设备 ─TCP─→ device-server ─LPUSH→ Redis ─BRPOP→ api-server
设备 ←TCP─ device-server ←BRPOP─ Redis ←LPUSH─ api-server

职责分层:

层 职责
device-server TCP连接管理、协议解析(HEX→结构化)、消息转发
api-server 业务逻辑:签名验证、状态持久化、充电事件、告警、指令下发

Redis 队列:

队列 方向 说明
device:reports 设备→平台 device-server 转发设备上报
device:commands 平台→设备 api-server 下发指令/响应

全局单位规范

login(设备→平台)

{"act":"login","msg_id":1,"dev_id":"123456789012345","sign":"abc12345","timestamp":1719800000,"iccid":"89860000000000000000"}
// 响应:{"suc":1,"msg_id":1,"cell":6}
  • iccid — SIM卡ICCID(变更时写 SIM 变更日志)
  • cell — 仓控板数量(柜子配置属性)

auth_str(设备→平台)

{"act":"auth_str","msg_id":2,"dev_id":"123456789012345"}
// 响应:{"suc":1,"msg_id":2,"auth_str":"abcd1234"}
  • auth_str — 8位随机安全码(仅返回一次,标记后不可重复获取)

status_post(设备→平台)

{"act":"status_post","msg_id":10,"dev_id":"123456789012345","rssi":85,
 "status":{"00":"0001","01":"000100020003000400050006","02":"000100020003000400050006"}}
// 响应:{"suc":1,"msg_id":10}

status 字段:

  • key: "00"=柜子级, "01"~"N"=仓控板级(N = cell-1)
  • value: 6通道状态拼接(每通道4个HEX字符=2字节)
  • device-server 解析后附加 parsed_status 字段,api-server 直接写入 Redis

HEX 解析(device-server 负责,格式待硬件团队最终确认):

  • 字节1 bit1=充电中, bit2=充满, bit3=故障
  • 当前实现:每通道4字符,取字节1解析

⚠️ HEX 编码格式待硬件团队最终确认,当前为预留实现。


pow_fail(设备→平台)

{"act":"pow_fail","msg_id":15,"dev_id":"123456789012345","info":"0"}
// 响应:{"suc":1,"msg_id":15}
  • info — 停电类型:0=直流停电(12V掉电),1=交流停电(380V掉电)

bat_in / bat_out(设备→平台)

{"act":"bat_in","msg_id":20,"id":"123456789012345-01-01","bms":{
  "soc":4500,"voltage":52000,"current":10000,"temp":29865,
  "battery_id":"BAT001","brand":"星恒","model":"XH-48V","sn":"SN123456"
}}
// 响应:{"suc":1,"msg_id":20}

{"act":"bat_out","msg_id":21,"id":"123456789012345-01-01","bms":{
  "battery_id":"BAT001","soc":4500
}}
// 响应:{"suc":1,"msg_id":21}

BMS 字段(全局整数单位):

字段 单位 精度 说明
soc ‰ 0.1% 4500 = 45.00%
voltage mV 1 52000 = 52.000V
current mA 1 10000 = 10.000A
temp K×100 0.01K 29865 = 298.65K = 25.5°C
battery_id — — 电池唯一标识
brand — — BMS品牌
model — — BMS型号
sn — — BMS序列号
cell_voltages mV 1 单体电芯电压数组

bat_out 只需上传 battery_id、soc(其他数据不变)。


on / off(双向)

// 平台→设备:下发充电
{"act":"on","msg_id":30,"id":"123456789012345-01-01"}
// 设备响应:{"suc":1,"msg_id":30}

// 设备→平台:充电开始确认
{"act":"on","msg_id":30,"id":"123456789012345-01-01"}
// 平台响应:{"suc":1,"msg_id":30}

// 平台→设备:下发停止
{"act":"off","msg_id":31,"id":"123456789012345-01-01"}
// 设备响应:{"suc":1,"msg_id":31}

// 设备→平台:充满自动停止
{"act":"off","msg_id":31,"id":"123456789012345-01-01"}
// 平台响应:{"suc":1,"msg_id":31}

on 必须由平台下发,设备不可主动充电。MCU 保证 off → bat_out 顺序。


open(平台→设备)

{"act":"open","msg_id":40,"id":"123456789012345"}

响应: {"suc":1,"msg_id":40}


pow_on / pow_off(平台→设备)

{"act":"pow_on","msg_id":50,"id":"123456789012345"}
{"act":"pow_off","msg_id":51,"id":"123456789012345"}

响应: {"suc":1,"msg_id":50}


reset(平台→设备)

// 重启整柜
{"act":"reset","msg_id":60,"id":"123456789012345"}
// 重启仓控板
{"act":"reset","msg_id":61,"id":"123456789012345-01"}

响应: {"suc":1,"msg_id":60}

充电结束原因

平台收到 判断原因
off (设备→平台) normal — 正常充满
off (平台→设备) user_stop — 用户手动停止
bat_out normal — 电池拔出
bat_in (通道已有电池) forced — 新电池插入强制结束
超时未响应 timeout — 通信超时

Redis 实时状态存储

层级结构

device:{imei}                              # 设备级 Hash
├─ online = "1"                            # 在线标记(TTL 300秒)
├─ rssi = "85"                             # 信号强度
├─ pow_fail_dc = "0/1"                     # 直流停电
├─ pow_fail_ac = "0/1"                     # 交流停电
└─ device:{imei}:board:{board_idx}         # 板级 Hash
   ├─ status_hex = "00010002..."           # 原始hex状态
└─ device:{imei}:board:{board_idx}:ch:{ch}  # 通道级 Hash(1~6)
       ├─ on = "0/1"                        # 充电中
      ├─ full = "0/1"                      # 充满
      ├─ fault = "0/1"                     # 故障
      ├─ hex = "0001"                      # 原始hex(4字符)
      ├─ battery_id = "BAT001"             # 电池ID(bat_in 写入,bat_out 清除)
      ├─ soc = "4500"                      # 电量 ‰
      ├─ voltage = "52000"                 # 电压 mV
      ├─ current = "10000"                 # 电流 mA
      ├─ temp = "29865"                    # 温度 K×100
      ├─ brand = "星恒"                    # BMS品牌
      ├─ model = "XH-48V"                  # BMS型号
      └─ sn = "SN123456"                   # BMS序列号

HEX 解析由 device-server 负责,api-server 只接收结构化数据写入 Redis。

BMS 字段(全局整数单位)

字段 单位 精度 说明
soc ‰ 0.1% 4500 = 45.00%
voltage mV 1 52000 = 52.000V
current mA 1 10000 = 10.000A
energy Wh 1 累计充电量
temp K×100 0.01K 29865 = 298.65K = 25.5°C
battery_id — — 电池唯一标识
brand — — BMS品牌
model — — BMS型号
sn — — BMS序列号
cell_voltages mV 1 单体电芯电压数组

权限码列表

以 software/api-server/src/db/migrate.rs::seed_permissions 的种子表为准(下表与之对齐)。 ⚠️ 历史上代码里曾误用一个不存在的权限码 device:door:unlock(种子里从来没有它)→ 导致开门对所有角色 403,已修正为 charge:open_door。

权限码 说明
device:view 查看设备
device:operate 操作设备(开合闸 / 重启 / 开门等柜级控制)
device:create 添加设备
device:edit 编辑设备(含修改充电工作模式)
device:delete 删除设备
device:auth_str:reset 重置安全码(仅总管理员)
charge:start 开始充电
charge:stop 停止充电
charge:open_door 开门(柜门解锁)
charge_record:view 查看充电记录
charge_record:export 导出充电记录
device_log:view 查看设备日志
energy:view 查看能耗
energy:export 导出能耗
user:view / user:create / user:edit / user:delete 用户管理
role:view / role:create / role:edit / role:delete 角色管理
operation_log:view 查看操作日志
org:view / org:create / org:edit / org:delete 组织管理