- user_scopes 由"假开关"做成生效:粒度到项目级(组织隔离 ∧ 项目∈范围,取交集);未配置=不限制(逐字向后兼容);
总管理员豁免;脏数据保守忽略不锁死;配置期校验存在性+归属;逐接口叠加(列表/详情/写操作/Excel 导出落 scope_project_ids);
前端新增"数据范围"弹窗入口。顺带修 GET /api/charge-orders 此前完全没有组织隔离(跨租户泄露)、
GET /api/projects/{id}/cabinets 的 500
- 数据库列类型系统性对齐:扫描并修复 pms_dev 32 项 / pms 18 项漂移(时间列统一 DATETIME,sqlx NaiveDateTime 只兼容 DATETIME);
DECIMAL→BIGINT 与列注释口径拉平;修复生产 GET /api/charge-orders、GET /api/device-logs 必然 500 的根因;
恢复 7 张表丢失的 updated_at ON UPDATE(行为变化,已记入 AGENTS.md);修正 SCHEMA_SQL 前向引用(空库建表必崩);
新增 tools/db_schema_diff.py(漂移检查,退出码可接部署前置)与 test/schema_alignment_e2e.py
- 安全加固四项:上传去 .svg + 魔数嗅探(堵存储型 XSS);登录失败锁定(5次/15分)+challenge 限流+并发闸门+验证码容差收紧;
服务降权 root→pms;nginx 安全头 + limit_req + CORS 白名单收敛
- TCP 连接登录门控(上行+下行同一规则):login 前只放行 auth_str/login(PRE_LOGIN_ALLOWED_ACTS)
- 状态位按归属层级展示:柜级 status1/status2 显示在柜子(含柜门 + 开门按钮,独立动作不联动业务)、
仓体级 batt_status/pow_status 显示在仓体;两端解析函数同源同名
- 能耗报表改实时聚合 charge_records(原 energy_stats 全表只读不写导致报表恒 0)+ 补复合索引
- 组织可见性:新增 GET /api/my-organizations(工厂全量、客户仅本组织)
- 仓体 status 仅表占用,充满/故障由 Redis 通道状态派生
- 订单/占用/队列运维缺口:h5 建单漏单、214 仓体自愈、device:reports 上限、死信 TTL、LimitNOFILE、
每日 mysqldump + 回滚点保留 5 次
- 文档:AGENTS.md 补层级可见性/数据范围/状态位约定/数据库工具/行为变化;docs/API.md 删除废弃 TCP 章节、接口总表对齐 mod.rs
35 KiB
API 接口文档
充电柜矩阵系统(PMS)接口规范。PC 管理端与微信小程序用户端共用同一套 API。
基础信息
- 测试环境:
https://test.anzhizhichong.com/pms/api/ - 正式环境:
https://app.anzhizhichong.com/pms/api/ - 认证方式:Bearer Token(JWT)
- 响应格式:
{ "code": 0, "data": ..., "message": "ok" }
统一端点清单
唯一事实来源:
software/api-server/src/routes/mod.rs的.route(...)注册清单。 本表一行 = 一次.route(...)注册,共 80 行,与Select-String -Path software\api-server\src\routes\mod.rs -Pattern '\.route\(' | Measure-Object得到的 80 条完全一致。同一次注册里链式绑定多个方法时,"方法"列列出全部方法(如GET / POST), 权限列按方法分别标注。权限码一律取自
db/migrate.rs::seed_permissions的种子表(共 31 个权限码)。 处理器内部没有调用check_permission的接口,如实标注为**"仅需登录"(只过认证中间件) 或"免认证"**(公开路由),不臆造权限码。⚠️
mod.rs里还有一条.nest_service("/uploads", ServeDir::new("uploads"))(静态头像目录,免认证), 它不是.route(...)注册,故不计入上表的 79 条。
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| POST | /api/auth/logout |
登出 | 仅需登录 |
| GET | /api/auth/me |
当前用户信息 | 仅需登录 |
| PUT | /api/auth/avatar |
更新头像 URL | 仅需登录 |
| POST | /api/auth/avatar/upload |
上传头像文件(multipart) | 仅需登录 |
| POST | /api/devices/{dev_id}/command |
下发设备指令 | device:operate |
| GET | /api/devices/stats |
设备服务节点统计 | device:view |
| POST | /api/nodes/register |
设备服务节点注册(device-server 调用) | 仅需登录 |
| POST | /api/nodes/{node_id}/heartbeat |
节点心跳 | 仅需登录 |
| POST | /api/nodes/{node_id}/deregister |
节点注销 | 仅需登录 |
| GET | /api/nodes |
节点列表 | 仅需登录 |
| GET / POST | /api/roles |
角色列表 / 创建角色 | GET 仅需登录;POST role:create |
| PUT / DELETE | /api/roles/{id} |
更新 / 删除角色 | role:edit / role:delete |
| PUT | /api/roles/{id}/permissions |
角色赋权 | role:edit |
| GET | /api/permissions |
权限码列表 | 仅需登录 |
| GET | /api/users/{id}/permissions |
用户权限(角色合并结果) | user:view |
| PUT | /api/users/{id}/roles |
分配角色 | user:edit |
| PUT / GET | /api/users/{id}/scopes |
设置 / 查询用户数据范围 | PUT user:edit;GET user:view |
| GET / POST | /api/organizations |
组织列表 / 创建组织 | org:view / org:create |
| PUT / DELETE | /api/organizations/{id} |
更新 / 删除组织 | org:edit / org:delete |
| PUT | /api/organizations/{id}/status |
禁用 / 启用组织 | 仅总管理员(代码内 role_level=2 判断,非权限码) |
| GET / POST | /api/organizations/{org_id}/projects |
项目列表 / 创建项目 | project:view / project:create |
| PUT / DELETE | /api/projects/{id} |
更新 / 删除项目 | project:edit / project:delete |
| GET | /api/projects/{project_id}/cabinets |
项目下设备列表 | device:view |
| POST | /api/cabinets |
批量创建柜子 | device:create |
| PUT / DELETE / GET | /api/cabinets/{id} |
更新 / 删除 / 设备详情(含仓板+仓体) | device:edit / device:delete / device:view |
| PUT | /api/cabinets/{id}/status |
启用 / 停用柜子 | device:edit |
| PUT | /api/cabinets/{id}/work-mode |
设置充电工作模式(0=平台指令,1=自动) | device:edit |
| POST | /api/cabinets/{id}/regenerate-auth |
重置安全码 | device:auth_str:reset(仅总管理员) |
| 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 |
| POST | /api/cabinets/{id}/bind |
绑定设备到项目 | device:edit |
| POST | /api/cabinets/{id}/replace-imei |
更换 IMEI | device:edit |
| GET | /api/organization-tree |
组织树(含项目/设备) | device:view |
| GET | /api/cabinets/filter |
设备列表(管理端,响应为 {total,data},见下) |
device:view |
| GET | /api/cabinets/realtime-status |
单设备实时状态(板级+通道级) | device:view |
| GET | /api/cabinets/options |
设备下拉选项 | device:view |
| GET | /api/cabin-boards/options |
仓控板下拉选项 | device:view |
| GET | /api/compartments/options |
仓体下拉选项 | device:view |
| 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/device-logs |
设备日志 | device_log:view |
| GET | /api/downloads |
下载中心列表 | charge_record:export |
| GET | /api/downloads/{id} |
下载任务详情 | charge_record:export |
| GET | /api/downloads/{id}/file |
下载文件 | charge_record:export |
| POST | /api/downloads/cleanup |
清理过期下载 | charge_record:export |
| 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:export |
| GET / POST | /api/users |
用户列表 / 创建用户 | user:view / user:create |
| PUT / DELETE | /api/users/{id} |
更新 / 删除用户 | user:edit / user:delete |
| PUT | /api/users/{id}/reset-password |
重置密码 | user:edit |
| PUT | /api/users/{id}/status |
设置用户状态 | user:edit |
| GET | /api/operation-logs |
操作日志 | operation_log:view |
| GET | /api/dashboard |
小程序首页仪表盘 | 仅需登录(内部按角色做组织隔离) |
| GET | /api/my-organizations |
小程序「选择组织」:当前用户可见的组织列表(工厂全量 / 客户仅本组织) | 仅需登录(不校验 org:view,内部按 role_level + users.organization_id 隔离) |
| GET | /api/cabinets |
设备列表(含实时 RSSI) | device:view |
| GET | /api/compartments/{id} |
仓体/通道详情(实时 BMS + 充电记录) | device:view |
| POST | /api/charge/start |
开始充电 | charge:start |
| POST | /api/charge/stop |
停止充电 | charge:stop |
| POST | /api/door/open |
开门(柜级:整柜共用一扇门,只解锁需人拉开) | charge:open_door |
| GET | /api/cabinets/scan/{abstract_id} |
扫码验证 | 仅需登录(组织归属校验,非权限码) |
| POST / GET | /api/feedback |
提交问题反馈 / 反馈列表 | POST 仅需登录;GET operation_log:view |
| PUT | /api/feedback/{id} |
更新反馈处理状态 | operation_log:view |
| POST | /api/feedback/upload |
上传反馈图片(multipart) | 仅需登录 |
| GET | /api/health |
健康检查 | 免认证 |
| GET | /api/auth/challenge |
获取登录挑战码 | 免认证 |
| POST | /api/auth/login |
登录 | 免认证 |
| POST | /api/auth/wx-login |
微信小程序手机号快速登录 | 免认证 |
| GET | /api/cabinets/abstract/{abstract_id} |
扫码查设备(二维码入口) | 免认证 |
| GET / POST | /api/wechat/callback |
微信服务器校验 / 消息回调 | 免认证 |
| GET | /api/maps/suggestion |
腾讯地图输入提示代理 | 免认证 |
| POST | /api/maps/geocoder |
腾讯地图逆地址解析代理 | 免认证 |
| GET | /api/maps/place-search |
腾讯地图地点搜索代理 | 免认证 |
列表响应包装(与实现逐条核对)
data 字段的结构并非全站统一,按实现如实记录如下:
| 接口 | data 结构 |
备注 |
|---|---|---|
GET /api/charge-records |
{list,total,page,page_size} |
标准分页 |
GET /api/charge-orders |
{list,total,page,page_size} |
标准分页 |
GET /api/batteries |
{list,total,page,page_size} |
标准分页 |
GET /api/device-logs |
{list,total,page,page_size} |
标准分页 |
GET /api/downloads |
{list,total,page,page_size} |
标准分页 |
GET /api/feedback |
{list,total,page,page_size} |
标准分页 |
GET /api/users |
{list,total} |
⚠️ 没有 page / page_size |
GET /api/operation-logs |
{list,total} |
⚠️ 没有 page / page_size |
GET /api/cabinets/filter |
{total,data} |
⚠️ 与其它列表不同:列表键名是 data 而不是 list,且无 page / page_size |
GET /api/energy-stats/cabinets |
[...] 数组 |
⚠️ data 直接是数组,无分页包装 |
GET /api/energy-stats/projects |
[...] 数组 |
⚠️ data 直接是数组,无分页包装 |
GET /api/energy-stats/trend |
{granularity,list} |
⚠️ 无 total / page / page_size |
GET /api/roles、GET /api/permissions |
[...] 数组 |
⚠️ data 直接是数组 |
GET /api/organization-tree |
[...] 树形数组 |
根节点数组 |
⚠️ 内部接口例外:
/api/nodes/*与/api/devices/*直接返回{"suc":1}/{"nodes":[...],"count":N}/{"node_count":N},不带{code,data,message}外层 —— 它们面向 device-server 内部调用,不是给前端消费的标准响应。
关键数值字段单位(抽查结论)
| 字段 | 出现位置 | 单位 | 前端显示 |
|---|---|---|---|
energy |
充电记录 / 充电订单 / 能耗统计 | Wh(整数) | / 1000 → kWh |
today_energy / month_energy |
能耗概览 | Wh(整数) | / 1000 → kWh |
max_temperature |
充电记录 | K×100(整数) | / 100 - 273.15 → °C |
soc / start_soc / end_soc |
电池台账 / 充电订单 / 通道状态 | ‰(整数,0–10000) | / 10 → % |
voltage / cell_voltages[] |
仓体详情 / BMS | mV | / 1000 → V |
current |
仓体详情 / BMS | mA | / 1000 → A |
rssi |
设备列表 / 通道状态 | 0–99 信号等级(不是 dBm) | 直接显示 |
estimated_cost |
能耗概览 | 元(f64,业务计算值) |
不转换 |
完整单位规范见文末 数据单位规范;后端不做任何单位换算,一律返回原始整数值。
认证接口
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/auth/challenge拿challenge_id,拖动对齐后把challenge_id+captcha_x(偏移量)一并提交给POST /api/auth/login; 缺challenge_id直接 400「缺少登录验证参数」,偏移量校验不过返回 400「验证码错误,请刷新页面重试」。
GET /api/auth/challenge
获取登录滑块验证码(免认证)。返回带缺口的背景图与拼图块图(data URL)。
{
"code": 0,
"data": {
"challenge_id": "b1f0...",
"bg_image": "data:image/png;base64,...",
"piece_image": "data:image/png;base64,..."
},
"message": "ok"
}
POST /api/auth/wx-login
微信小程序手机号快速登录(免认证)。前端先 wx.login() 拿 code、getPhoneNumber 拿 phone_code;
后端向微信换取 openid + 手机号,按手机号匹配用户后签发 JWT,并把 openid 写回 users.openid。
请求: { "code": "081...", "phone_code": "e3f..." }
响应: 与 POST /api/auth/login 完全同构({token, user:{id,name,phone,role_level,organization_id,permissions}}),
message 为「登录成功」;账户被禁用返回 403「账户已被禁用」。
PUT /api/auth/avatar
更新当前用户头像 URL(仅需登录)。请求: { "avatar": "https://.../a.png" };
响应: { "code": 0, "data": { "avatar": "https://.../a.png" }, "message": "ok" }。
POST /api/auth/avatar/upload
上传头像文件(仅需登录,multipart/form-data)。
- 大小上限 2MB,超限 400「头像文件不能超过 2MB」;内容为空 400「上传内容为空」;
- 扩展名白名单
.jpg/.jpeg/.png/.gif/.webp/.svg,不在白名单内一律回落为.jpg(不是报错); - 无扩展名时默认
.jpg。
响应: { "code": 0, "data": { "avatar": "/uploads/avatars/<uuid>.jpg" }, "message": "ok" }
(文件落盘为 uploads/avatars/<uuid><ext>,同时写回 users.avatar)
⚠️ 静态文件目录由
mod.rs的.nest_service("/uploads", ServeDir::new("uploads"))提供,免认证。 代码里该处理器的文档注释误写成GET,实际注册的是POST(以mod.rs为准)。
小程序专用接口
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}
扫码验证,需认证。验证当前用户是否有权访问该设备(校验组织归属;无独立权限码)。
问题反馈
| 接口 | 说明 | 权限 |
|---|---|---|
POST /api/feedback |
提交反馈(content 必填,≤2000 字,可带 images) |
仅需登录(任何已登录用户可提交) |
GET /api/feedback |
反馈列表({list,total,page,page_size}) |
operation_log:view |
PUT /api/feedback/{id} |
更新处理状态 | operation_log:view |
POST /api/feedback/upload |
上传反馈图片(multipart/form-data) |
仅需登录 |
POST /api/feedback 响应:{ "code": 0, "data": { "success": true }, "message": ... };
POST /api/feedback/upload 响应:{ "code": 0, "data": { "url": "..." }, "message": "ok" }。
管理端接口
用户数据范围(user_scopes)— PUT / GET /api/users/{id}/scopes
权限:PUT 需 user:edit;GET 需 user:view(总管理员 role_level >= 2 因 check_permission 短路而全通过)。
语义权威定义见 AGENTS.md「组织架构 → 数据范围(user_scopes)」。
范围语义(四条硬规则,不得擅改):
| 情形 | 行为 |
|---|---|
| 未配置任何记录 | 不限制(沿用组织隔离,与未引入该机制时完全一致)—— 绝不是"无权限" |
| 已配置(≥1 条有效) | 在组织隔离基础上再收窄(交集),绝不放大可见范围 |
总管理员(role_level >= 2) |
豁免:始终可见全量,配置保存但不生效 |
| 有记录但无一条有效(脏数据/设备级/跨组织/项目已删) | 保守忽略无效行;若一条有效都不剩 ⇒ 按"未配置 = 不限制"处理并记 WARN |
粒度到项目级:scope_type 1=组织(只允许配成本人所属组织,展开为该组织全部项目)、
2=项目(实际生效粒度)、3=设备/柜子(不参与鉴权,读取时忽略、不生成任何过滤条件)。
PUT 请求体:
{
"scopes": [
{ "scope_type": 2, "scope_id": 12, "permission_type": 1 }
]
}
| 字段 | 说明 |
|---|---|
scope_type |
1 组织 / 2 项目 / 3 设备(设备级存档但不参与鉴权) |
scope_id |
对应资源 ID,必须存在且属于该用户所在组织,否则 400 |
permission_type |
1 查看 / 2 操作(当前两者都只定义"可见边界",不区分读/写) |
- 传
"scopes": []= 清空范围 = 恢复"不限制"; - 全量替换,在一个事务内执行(校验全部通过前不删旧数据);
- 校验失败示例:
400{"code":400,"message":"项目 99 不属于该用户所属组织,无法配置为其数据范围"}; 目标用户不存在 →404;scope_type/permission_type越界 →400。
GET 响应(返回库里存量记录,另附 valid 标记该行是否会被鉴权消费):
{
"code": 0,
"data": [
{ "id": 3, "scope_type": 2, "scope_id": 12, "permission_type": 1, "valid": true },
{ "id": 4, "scope_type": 3, "scope_id": 7, "permission_type": 1, "valid": true }
],
"message": "ok"
}
valid的含义是「这条记录会不会被鉴权消费」:
false= 不会生效 —— 资源已删除 / 跨组织 / 无效类型,或scope_type=3(设备级,永远不参与鉴权);- 鉴权时这些行被忽略;管理端可据此提示管理员清理。
GET /api/users/{id}/permissions的scopes字段仍为原始数组(不带valid)。
受数据范围影响的接口(组织条件之后再叠加项目级收窄;未配置时不产生任何额外条件):
| 接口 | 收窄落点 |
|---|---|
GET /api/cabinets/filter、GET /api/projects/{id}/cabinets、GET /api/cabinets/options |
cabinets.project_id |
GET /api/cabinets/{id}、GET /api/cabinets/realtime-status |
归属校验(不在范围内 → 403) |
GET /api/charge-records、GET /api/charge-records/export |
charge_records.cabinet_id → 项目 |
GET /api/charge-orders |
charge_orders.cabinet_id → 项目 |
GET /api/device-logs |
device_logs.cabinet_id → 项目 |
GET /api/energy-stats/{summary,cabinets,projects,trend,export} |
项目/柜子维度 |
GET /api/organizations/{org_id}/projects |
projects.id |
所有柜子写操作(PUT/DELETE /api/cabinets/{id}、work-mode、status、bind、replace-imei、regenerate-auth、/api/devices/{dev_id}/command、/api/projects/{id}) |
scope_guard(不在范围内 → 403) |
小程序:GET /api/cabinets、GET /api/dashboard、GET /api/compartments/{id}、POST /api/charge/*、POST /api/door/open、/api/cabinets/{id}/{open,pow-on,pow-off,reset} |
设备列表按项目过滤;详情/写操作归属校验 → 403 |
GET /api/operation-logs不叠加数据范围:操作日志按user_id → users.organization_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=手动停止
单位: energy = Wh(整数,前端 / 1000 显示 kWh);max_temperature = K×100(整数,/ 100 - 273.15 显示 °C)。
start_time / end_time 为字符串时间,非数值单位。
GET /api/cabinets/filter
设备列表(管理端)。响应包装与其它列表接口不同:data 是 {total,data},
列表键名是 data 而不是 list,且没有 page / page_size。
{
"code": 0,
"data": {
"total": 2,
"data": [
{ "id": 1, "abstract_id": "10-00000000", "name": "太湖新城换电柜A", "status": 1, "online": 1, "rssi": 85 }
]
},
"message": "ok"
}
⚠️ 前端解析时不要套用
{list,total,page,page_size}的通用分页逻辑。
PUT /api/cabinets/{id}/work-mode
设置柜子充电工作模式。权限 device:edit + 组织归属校验。
请求: { "work_mode": "0" }(字符串,前端契约)—— 白名单取值,"0"=平台指令模式(默认)、
"1"=自动充电模式;其它值(含 "2"/"true")返回 400。
响应: { "code": 0, "data": { "success": true }, "message": "ok" }
PUT /api/cabinets/{id}/status
禁用 / 启用柜子。权限 device:edit + 组织归属校验,且仅总管理员(role_level=2)可执行,
否则 403「仅总管理员可禁用/启用柜子」。
请求: { "status": 2 };响应: { "code": 0, "data": null, "message": "ok" }
PUT /api/organizations/{id}/status
禁用 / 启用组织。仅总管理员(代码内 role_level < 2 判断,不走权限码);
禁用后该组织及其子组织的柜子全部不可用(指令链路拦截)。
请求: { "disabled": 1 }(only 0/1,其它值 400);响应: { "code": 0, "data": null, "message": "禁用成功" }
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": 200,
"end_soc": 950,
"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=拔出结束
单位: energy = Wh(整数);start_soc / end_soc = ‰(整数,0–10000,前端 / 10 显示 %)。
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": 4500,
"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"
}
单位: voltage = mV(52000 = 52.000V);soc = ‰(4500 = 45.00%,表字段注释即 电量(‰))。
⚠️ 历史文档曾把示例写成 "soc": 45,那是按"百分比"写的错误示例,实现返回的是千分比。
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单价计算
免认证接口与内部接口
本节全部来自
mod.rs::public_routes()(免认证,不经过认证中间件)与节点管理接口。
GET /api/health
健康检查(免认证)。
GET / POST /api/wechat/callback
微信服务器回调(免认证):GET 做服务器地址校验(回显 echostr),POST 接收消息(明文 XML)。
Nginx 有独立的 /api/wechat/ location 直连 API 服务,不经 /api/pms/ 前缀重写。
腾讯地图代理(免认证,前端需要)
| 接口 | 方法 | 说明 |
|---|---|---|
/api/maps/suggestion |
GET | 输入提示(keyword 联想) |
/api/maps/geocoder |
POST | 逆地址解析(经纬度 → 地址) |
/api/maps/place-search |
GET | 地点搜索 |
代理的目的:把腾讯地图 Key 留在服务端,前端不暴露 Key。
设备服务节点与指令接口(响应不含标准外层)
| 接口 | 方法 | 权限 | 响应 |
|---|---|---|---|
/api/nodes/register |
POST | 仅需登录 | {"suc":1} |
/api/nodes/{node_id}/heartbeat |
POST | 仅需登录 | {"suc":1};未注册 {"suc":0,"err":"节点未注册"} |
/api/nodes/{node_id}/deregister |
POST | 仅需登录 | {"suc":1} |
/api/nodes |
GET | 仅需登录 | {"nodes":[...],"count":N} |
/api/devices/stats |
GET | device:view |
{"node_count":N} |
/api/devices/{dev_id}/command |
POST | device:operate |
{"suc":1} |
nodes/*是 device-server 内部调用;devices/*是管理端/后端调用。两者都不返回{code,data,message}标准外层(见上表"内部接口例外")。
数据单位规范
核心原则:所有数值字段使用整数存储,禁止浮点类型 (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 设备通讯协议
TCP 设备通讯协议见 柜控板固件对接文档.md(唯一权威,v1.1)。本文不再重复。
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的种子表为准(下表与之对齐,共 31 个权限码)。 ⚠️ 历史上代码里曾误用一个不存在的权限码device:door:unlock(种子里从来没有它)→ 导致开门对所有角色 403,已修正为charge:open_door。 ⚠️ 同样地,不存在system:log这个权限码(文档曾误写)——操作日志用的是operation_log:view; 照system:log配角色会导致该角色永远看不到操作日志。默认角色授权(
seed_roles):
super_admin(总管理员):grant_all_permissions—— 种子表全部 31 个;org_admin(企业管理员):24 个 ——device:view/operate/create/edit、charge:start/stop/open_door、charge_record:view/export、device_log:view、energy:view/export、user:view/create/edit、operation_log:view、org:*(4)、project:*(4); 不含device:delete、device:auth_str:reset、user:delete、role:*;normal_user(操作员):8 个 ——device:view/operate、charge:start/stop/open_door、charge_record:view、device_log:view、energy:view。
| 权限码 | 说明 |
|---|---|
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 |
导出充电记录(也用于下载中心 4 个接口) |
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 |
查看操作日志(也用于问题反馈列表/处理状态 2 个接口) |
org:view / org:create / org:edit / org:delete |
组织管理 |
project:view / project:create / project:edit / project:delete |
项目管理 |