12451 ee92102f3c feat(scope)+fix(db)+feat(security): 数据范围(项目级)生效、库列类型对齐、安全加固、状态位分层展示、能耗实时聚合
- 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
2026-09-20 02:44:51 +08:00

35 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" }

统一端点清单

唯一事实来源: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 项目管理