1311 lines
68 KiB
Markdown
Raw Normal View History

# API 接口文档
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:40:46 +08:00
> 充电柜矩阵系统(PMS)接口规范。PC 管理端与微信小程序用户端共用同一套 API。
## 基础信息
- **测试环境**:`https://test.anzhizhichong.com/pms/api/`
- **正式环境**:`https://app.anzhizhichong.com/pms/api/`
- **认证方式**:Bearer Token(JWT)
- **响应格式**:`{ "code": 0, "data": ..., "message": "ok" }`
## 统一端点清单
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> **唯一事实来源**:`software/api-server/src/routes/mod.rs` 的 `.route(...)` 注册清单。
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
> 本表**一行 = 一次 `.route(...)` 注册**,共 **81** 行,与
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> `Select-String -Path software\api-server\src\routes\mod.rs -Pattern '\.route\(' | Measure-Object`
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
> 得到的 **81** 条完全一致。同一次注册里链式绑定多个方法时,"方法"列列出全部方法(如 `GET / POST`),
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> 权限列按方法分别标注。
>
> 权限码**一律取自 `db/migrate.rs::seed_permissions` 的种子表**(共 31 个权限码)。
> 处理器内部没有调用 `check_permission` 的接口,如实标注为**"仅需登录"**(只过认证中间件)
> 或**"免认证"**(公开路由),不臆造权限码。
>
> ⚠️ `mod.rs` 里还有一条 `.nest_service("/uploads", ServeDir::new("uploads"))`(静态头像目录,**免认证**),
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
> 它不是 `.route(...)` 注册,故不计入上表的 **81** 条。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
>
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
> 上表 81 行按 `(方法, 路径)` 展开后共 **94** 对,与 `mod.rs` 的 94 对**逐条双向一致**
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> (用 `(方法,路径)` 多重集比对,**不按路径合并** —— 例如 `/api/cabinets` 被注册两次:
> `cabinet_routes` 里 `POST` 建柜、`h5_routes` 里 `GET` 小程序设备列表,二者是两条独立路由)。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| POST | `/api/nodes/register` | 节点注册(**device-server 不调用**,见下) | **仅总管理员**(`role_level >= 2`) |
| POST | `/api/nodes/{node_id}/heartbeat` | 节点心跳 | **仅总管理员** |
| POST | `/api/nodes/{node_id}/deregister` | 节点注销(**会真删注册表条目**) | **仅总管理员** |
| GET | `/api/nodes` | 节点列表(**含内网 `ip:tcp_port`**) | **仅总管理员** |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` |
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:40:46 +08:00
| 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` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| POST | `/api/cabinets/{id}/bind` | 绑定设备到项目 | `device:edit` |
| POST | `/api/cabinets/{id}/replace-imei` | 更换 IMEI | `device:edit` |
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
| GET | `/api/cabinets/{id}/meter-usage` | **整柜日用电量**(电表读数口径,与 `energy-stats` 的订单电量口径是两套数据) | `device:view` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` |
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:40:46 +08:00
| GET | `/api/charge-orders` | 充电订单列表 | `charge_record:view` |
| GET | `/api/batteries` | 电池台账列表 | `device:view` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` |
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:40:46 +08:00
| 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` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` | 小程序首页仪表盘 | 仅需登录(内部按角色做组织隔离) |
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
| GET | `/api/my-organizations` | 小程序「选择组织」:当前用户**可见**的组织列表(工厂全量 / 客户仅本组织) | 仅需登录(不校验 `org:view`,内部按 `role_level` + `users.organization_id` 隔离) |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| GET | `/api/cabinets` | 设备列表(含实时 RSSI) | `device:view` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| GET | `/api/compartments/{id}` | 仓体/通道详情(实时通道状态 + 充电记录) | `device:view` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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) | 仅需登录 |
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:40:46 +08:00
| GET | `/api/health` | 健康检查 | 免认证 |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 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` 直接是数组 |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `GET /api/organizations`、`GET /api/my-organizations` | `[...]` 数组 | ⚠️ `data` 直接是组织数组(字段 `id`/`name`) |
| `GET /api/cabinets`(**小程序**) | `[...]` 数组 | ⚠️ **无任何包装**,也没有分页参数;**与 `GET /api/cabinets/filter` 的 `{total,data}` 不同** |
| `GET /api/cabinets/options`、`GET /api/cabin-boards/options`、`GET /api/compartments/options` | `[...]` 数组 | ⚠️ `data` 直接是数组 |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `GET /api/organization-tree` | `[...]` 树形数组 | 根节点数组 |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `GET /api/cabinets/realtime-status` | `{device,boards}` | ⚠️ 无列表/分页 |
| `GET /api/energy-stats/summary` | `{today_energy,month_energy,estimated_cost,cabinet_count,online_count}` | 单对象 |
| `GET /api/users/{id}/scopes` | `[...]` 数组 | ⚠️ `data` 直接是范围记录数组(带 `valid`) |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> ⚠️ **内部接口例外**:`/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` → % |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `voltage` | 仓体详情 / 通道状态 | **mV** | `/ 1000` → V |
| `current` | 仓体详情 / 通道状态 | **mA** | `/ 1000` → A |
| `temp` | 仓体详情 / 设备列表 / 设备详情(柜温与电芯最高温同口径) | **K×100** | `/ 100 - 273.15` → °C |
| `meter` | 设备级 Redis(协议 `cabinet.meter`,未在 HTTP 响应透出) | **Wh**(电表读数) | `/ 1000` → kWh |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `rssi` | 设备列表 / 通道状态 | **0–99 信号等级**(不是 dBm) | 直接显示 |
| `estimated_cost` | 能耗概览 | **元**(`f64`,业务计算值) | 不转换 |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `cell_voltages[]` / `cell_temps[]` | 仓体详情 | ⚠️ **恒为空数组**(无写入方,保留字段类型) | — |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> 完整单位规范见文末 [数据单位规范](#数据单位规范);后端**不做任何单位换算**,一律返回原始整数值
> (唯一例外是 `estimated_cost`,见 `GET /api/energy-stats/summary`)。
---
## 认证接口
### POST /api/auth/login
**请求:**
```json
{
"phone": "13800138000",
"password": "admin123"
}
```
**响应:**
```json
{
"code": 0,
"data": {
"token": "eyJ0eXAi...",
"user": {
"id": 1,
"name": "管理员",
"phone": "13800138000",
"role_level": 2,
"organization_id": null,
"permissions": ["device:view", "device:create", "..."]
}
},
"message": "登录成功"
}
```
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:40:46 +08:00
**角色等级:** 0=操作员,1=企业管理员,2=总管理员
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> ⚠️ 登录**必须**带滑块验证码:先调 `GET /api/auth/challenge` 拿 `challenge_id`,拖动对齐后把
> `challenge_id` + `captcha_x`(偏移量)一并提交给 `POST /api/auth/login`;
> 缺 `challenge_id` 直接 400「缺少登录验证参数」,偏移量校验不过返回 400「验证码错误,请刷新页面重试」。
### GET /api/auth/challenge
获取登录滑块验证码(**免认证**)。返回带缺口的背景图与拼图块图(data URL)。
```json
{
"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` 为准)。
---
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:40:46 +08:00
## 小程序专用接口
### GET /api/dashboard
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:40:46 +08:00
小程序首页仪表盘数据。支持 `org_id` 参数按组织过滤。
**响应:**
```json
{
"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 }
]
}
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
### GET /api/my-organizations
小程序「选择组织」弹层专用:返回当前用户**可见**的组织列表。
- **权限:仅需登录**(`routes/h5.rs::list_my_organizations` 里**没有** `check_permission` 调用),
**不校验 `org:view`** —— 操作员的授权清单里没有该码,若沿用管理端
`GET /api/organizations`(硬校验 `org:view`)会 **403、组织弹层永远为空**;
- **隔离规则**(`visible_org_scope`,按 `role_level` + `users.organization_id`):
工厂(`role_level >= 2`)**全量**;组织管理员 `1` / 操作员 `0` **仅 `users.organization_id` 指向的那一个**;
`organization_id` 为 `NULL` → **空数组**(不是错误);
- **与管理端 `GET /api/organizations` 的分工不可混用**:后者语义冻结(`org:view` + 全量)。
**响应:** `data` 直接是组织数组(与管理端同构,字段 `id` / `name`):
```json
{
"code": 0,
"data": [
{ "id": 1, "name": "无锡某某换电科技有限公司" }
],
"message": "ok"
}
```
### GET /api/cabinets
设备列表,支持 `project_id` 和 `org_id` 过滤。响应包含实时 RSSI。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> ⚠️ **`data` 是数组本身**(`{"code":0,"data":[...],"message":"ok"}`),**不是** `{list,total}` 包装,
> 也没有分页参数 —— 与 `GET /api/cabinets/filter` 的 `{total,data}` 完全不同,前端不要套通用分页逻辑。
**数组元素字段**(依据 `routes/h5.rs::build_cabinet_list`):
```json
{
"id": 1,
"abstract_id": "10-00000001",
"name": "太湖新城换电柜A",
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"imei": "864720562557021",
"iccid": "8986012520983901841",
"status": 0,
"project_id": 1,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"address": "无锡市新吴区…",
"latitude": "31.550000",
"longitude": "120.360000",
"channel_count": 36,
"idle_count": 30,
"charging_count": 6,
"full_count": 0,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"fault_count": 0,
"rssi": 85,
"pow_fail_dc": false,
"pow_fail_ac": false,
"temp": 29800,
"status1": "10",
"status2": null,
"org_disabled": false
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
- `status`:柜子级状态,**沿用 `cabinets.status`**(`0`/`1`/`2`,`2`=禁用,见
`PUT /api/cabinets/{id}/status`),**不是**仓体的 `0/1/2/3` 派生档;
- `idle_count` / `charging_count`:**来自 DB `compartments.status` 的占用值**(`0`/`1`),
不做派生覆盖(`derive_channel_counts` 派生出的 idle/charging 分区不写回响应);
- `full_count` / `fault_count`:**由 Redis 通道状态派生**(`batt_status` bit0=充满、
bit4/5/6 或 `pow_status` bit0=故障);设备离线/无通道数据时**恒为 0**(无法判定,不猜测);
- `status1` / `status2`:**柜级**状态位(协议 §3.8),2 位小写 hex **原样透出**,
**取不到为 `null`(不是 `"00"`)** —— `"00"` 是"全部正常"的有效值,两者语义不同;
- `temp`:柜温,单位 **K×100**(`29800` = 25.5 °C),取不到为 `null`;
- `pow_fail_dc` / `pow_fail_ac`:布尔,取不到为 `null`;
- `org_disabled`:所属项目对应组织是否被禁用(前端徽标用)。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### GET /api/cabinets/abstract/{abstract_id}
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
扫码入口,**免认证**(注册在 `mod.rs::public_routes()`,不经过认证中间件;
`mod.rs:211` → `h5::get_cabinet_by_abstract_id`)。通过二维码中的抽象 ID 查找设备。
**响应(最小字段集):** `data` 恰好 5 个键 —— `id` / `abstract_id` / `name` / `status` / `cabin_boards`;
`cabin_boards[]` 每项 4 个键 —— `id` / `board_id` / `status` / `compartments[]`;
`compartments[]` 每项 3 个键 —— `id` / `channel_id` / `status`。
```json
{
"code": 0,
"data": {
"id": 1,
"abstract_id": "10-00000001",
"name": "太湖新城换电柜A",
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"status": 0,
"cabin_boards": [
{
"id": 1,
"board_id": 1,
"status": 0,
"compartments": [
{ "id": 1, "channel_id": 1, "status": 0 }
]
}
]
}
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> 🔓 **为什么必须是"最小集"**:本接口**没有 token 也能调**,所以响应刻意只给**扫码进入充电流程必需**的字段 ——
> `id`(后续 `POST /api/charge/start` / `POST /api/door/open` 要用的**内部主键**)与拓扑,
> 用来渲染"哪个仓可用"。**不含**任何设备身份或归属信息:
> `imei` / `iccid` / `auth_str`(敏感凭据)/ `project_id` / `organization_id` / `address` /
> `latitude` / `longitude` / `rssi` / 实时状态。依据:`h5.rs::get_cabinet_by_abstract_id` 的 SQL
> 只 `SELECT id, abstract_id, imei, name, status`,且把 `imei` 绑到 **`_imei` 后丢弃**,其余列根本没查。
> 需要完整设备信息请走需认证的 `GET /api/cabinets/{id}`。
> 这里的仓体 `status` 是 **DB 原始占用值**(`0`/`1`),未做实时派生(本接口不读 Redis)。
> 🔒 **`abstract_id` 可枚举 —— 已加 nginx 限流(2026-09 上线,含实测依据)**
>
> 格式固定为 `10-%08d`(如 `10-00000001`),即 **1 亿个可穷举的连续 ID**;本接口免认证,
> 攻击者无需 token 即可逐个探测"柜子是否存在 + 柜名 + 仓体结构"。
> **现已由 nginx 限流兜住**:
>
> ```nginx
> # /etc/nginx/nginx.conf
> limit_req_zone $binary_remote_addr zone=scan_abstract_zone:10m rate=30r/m;
> limit_req_status 429;
>
> # conf.d/{app,test}.anzhizhichong.com.conf(两份都配了)
> location /api/pms/cabinets/abstract/ {
> limit_req zone=scan_abstract_zone burst=20 nodelay;
> proxy_pass http://127.0.0.1:9000/api/cabinets/abstract/; # 测试域为 :3000
> }
> ```
>
> - **`zone=scan_abstract_zone`**,`rate=30r/m` + `burst=20 nodelay`(比登录/验证码宽松得多:
> 正常扫码是"一次扫码 1~2 个请求",同一 IP 连扫 20 次不排队),超限返回 **`429`**
> (`limit_req_status 429`);
> - **实测(2026-09,本机同 IP 连发 60 次 `GET /api/pms/cabinets/abstract/10-99999999`)**:
> - `test.anzhizhichong.com`:**`404` × 23 + `429` × 37**
> - `app.anzhizhichong.com`:**`404` × 23 + `429` × 37**
> (取证前先单发一次得 `404`,确认 1 分钟窗口已回填、不是"假拦截");
> - 23 次通过 ≈ `burst=20` + 窗口内回填,符合配置预期;
> - `nginx -t` 通过;仓库镜像 `deploy/nginx/{app,test}.anzhizhichong.com.conf` 与线上一致。
> - ⚠️ 顺序前提:本节的 `location` 是**前缀匹配**,`/api/pms/cabinets/abstract/` 与
> 需认证的 `/api/pms/cabinets/scan/` 是两条独立路径,**限流只覆盖前者**。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### GET /api/compartments/{id}
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
仓体/通道详情,含通道实时状态与充电记录(`routes/h5.rs::get_compartment_detail`)。
**响应字段:**
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
```json
{
"code": 0,
"data": {
"id": 1,
"channel_id": 1,
"cabin_board_id": 1,
"status": 2,
"voltage": 54600,
"current": 0,
"energy": null,
"soc": 1000,
"temp": 29800,
"batt_status": "81",
"pow_status": "02",
"cell_voltages": [],
"cell_temps": [],
"bms_alerts": [],
"charge_records": []
},
"message": "ok"
}
```
- `status` — **派生值 `0 空闲 / 1 充电中 / 2 充满 / 3 故障`**(优先级 `fault > full > charging > idle`,
由 Redis 通道 `batt_status` / `pow_status` 经 `cabinet_tree::derive_channel_flags` 推导)。
**拿不到通道实时数据时回落到 DB 的占用值 `0`/`1`**(离线无法判定,不猜测);
**没有"禁用"这一档**(仓体级禁用不存在)。
- `voltage` — mV(整数);`current` — mA(整数);`temp` — **K×100**(整数);
`soc` — ‰(0–10000,前端 `/ 10` 转 %)。**取不到为 `null`,绝不落 `0`**。
- `batt_status` / `pow_status` — 通道 18 字符 hex 的 **byte0 / byte1**,2 位小写 hex 原样透出
(位定义见协议 §3.8),取不到为 `null`。
- `energy` — **恒为 `null`**:通道级 Redis Hash **没有**累计电量字段(累加器是订单维度的内部键),
此处如实返回 null 而不是 `0`。
- `cell_voltages` / `cell_temps` / `bms_alerts` — **恒为空数组 `[]`**:
协议未上报 BMS 单体明细,**当前没有任何写入方**,保留字段仅为维持既有前端类型(`number[]`/`string[]`)。
### POST /api/charge/start
**请求:** `{ "compartment_id": 123 }`
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:40:46 +08:00
> 权限:`charge:start` + 组织隔离(用户只能操作所属组织的设备)
### POST /api/charge/stop
**请求:** `{ "compartment_id": 123 }`
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:40:46 +08:00
> 权限:`charge:stop` + 组织隔离
### POST /api/door/open
**请求:** `{ "compartment_id": 123 }`
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:40:46 +08:00
> 权限:`charge:open_door` + 组织隔离
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### GET /api/cabinets/scan/{abstract_id}
扫码验证,需认证。验证当前用户是否有权访问该设备(校验**组织归属**;无独立权限码)。
### 问题反馈
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| 接口 | 说明 | 权限 |
|------|------|------|
| `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" }`。
---
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:40:46 +08:00
## 管理端接口
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
### 用户数据范围(`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 请求体**:
```json
{
"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` 标记该行是否会被鉴权消费):
```json
{
"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` → 项目 |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `GET /api/batteries` | **间接归属**:`battery_id` → `charge_orders` → `cabinets.project_id`(叠加在同一个 `EXISTS` 子查询里;语义见该接口一节) |
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
| `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` 归属,
> 与项目/设备无关联列,项目级范围无法映射(经确认,非遗漏)。
---
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:40:46 +08:00
### 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 | 结束时间(含),格式同上 |
**响应:**
```json
{
"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,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"status": 0,
"status_text": "正常"
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:40:46 +08:00
}
],
"total": 50,
"page": 1,
"page_size": 20
},
"message": "ok"
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
**状态码:** `0` 正常 / `1` 异常 / `2` 手动停止(仅 3 档;`charge_records.status` 列注释即
`0正常 1异常 2手动停止`,`routes/charge_records.rs::status_text` 逐值对应,越界返回 `"未知"`)。
⚠️ 历史文档曾写成"0=充电中,1=正常结束,2=异常中断,3=手动停止",**四档且值全错** ——
`charge_records` **没有"充电中"档**(充电中的会话在 `charge_orders`,`status=0`)。
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:40:46 +08:00
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
**单位:** `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`。
```json
{
"code": 0,
"data": {
"total": 2,
"data": [
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
{
"id": 1,
"abstract_id": "10-00000000",
"name": "太湖新城换电柜A",
"status": 1,
"is_online": true,
"rssi": 85,
"pow_fail_dc": false,
"pow_fail_ac": false,
"status1": "10",
"status2": null,
"idle_count": 30,
"charging_count": 6,
"full_count": 2,
"fault_count": 0
}
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
]
},
"message": "ok"
}
```
> ⚠️ 前端解析时不要套用 `{list,total,page,page_size}` 的通用分页逻辑。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
>
> - 在线字段名是 **`is_online`**(布尔,Redis `online == "1"`;Redis 不可用/无键时为 `null`),
> **不是 `online`**;
> - `status1` / `status2`:**柜级**状态位,2 位 hex 原样透出,**取不到为 `null`(不是 `"00"`)**;
> - `idle_count` / `charging_count`:**DB 占用值**(`compartments.status` 的 `0`/`1`),不被派生覆盖;
> - `full_count` / `fault_count`:**由 Redis 通道状态派生**后**覆盖** SQL 返回值,
> 设备离线/无通道数据时**按 0 计**(`cabinet_tree::apply_derived_counts`);
> - 查询参数:`project_id` / `organization_id`(**含其直接子组织**)/ `bound`(`"1"` 已绑定 / `"0"` 未绑定)。
> 🔒 **`?project_id=` / `?organization_id=` 是「纯筛选参数」,不构成归属边界**(2026-09 修复):
> 它们只表达"我想看哪个项目/组织",**决定"能不能看"的永远是另外两道**,且**无条件叠加**:
> 1. **组织隔离**(`auth::org_condition_for_project`)—— 总管理员豁免(全量);
> 客户侧收窄到 `users.organization_id`;**无组织则 `1=0`(空集,绝不退化成全量)**;
> 2. **数据范围**(`user_scopes` 项目白名单,`effective_scope()`)—— 未配置 = 不限制。
>
> 三道条件是 `AND` 关系**取交集**(书写顺序 = 占位符顺序 = 绑定值顺序,见 `build_filter_where`)。
> **传别人的 `project_id` / `organization_id` 只会得到 `{"data":[],"total":0}`,不会越权。**
> `total` 与列表走同一个 `where_clause`,不存在"总数含不可见行"。
>
> ⚠️ **修复前的真实越权(历史缺陷,勿再退回)**:旧实现把这两个入参**直接当作唯一过滤条件**
> (`WHERE c.project_id = ?` / `JOIN projects ... WHERE p.organization_id = ?`),
> **没有叠加组织隔离** —— 客户账号只要传别人的 `project_id`,就能拿到**对方柜子的全字段**
> (含 `imei` / `iccid` / `address` / 经纬度)。这与数据范围 `user_scopes` 无关:
> **未配置 scope 的用户同样能被利用**。现已改为"入参只当筛选、边界无条件叠加"。
### GET /api/cabinets/realtime-status
单设备实时状态(板级 + 通道级),权限 `device:view` + **设备归属校验**
(非总管理员必须命中「设备 → 项目 → 组织」链路,且项目落在数据范围内,否则 `403`)。
**查询参数:** `imei`(必填)
```json
{
"code": 0,
"data": {
"device": {
"online": true,
"rssi": 85,
"pow_fail_dc": false,
"pow_fail_ac": false
},
"boards": {
"1": {
"status_hex": "820281028002",
"channels": {
"1": { "on": true, "full": false, "fault": false, "status": "charging",
"hex": "8202", "batt_status": "82", "pow_status": "02",
"soc": 4500, "voltage": 54600, "current": 0, "temp": 29800 }
}
}
}
},
"message": "ok"
}
```
- **`device` 只有 4 个字段**:`online`(布尔,**缺失时 `false` 而不是 `null`**)、
`rssi`(取不到为 `null`)、`pow_fail_dc` / `pow_fail_ac`(**缺失时 `false`**)。
⚠️ **本接口不返回 `cabinet_status1/2`,也不返回柜温 `temp`** ——
要柜级状态位/柜温请用 `GET /api/cabinets/{id}` 或 `GET /api/cabinets`(小程序列表);
- `boards` 的键是**仓控板号字符串**(`"1"`…),`channels` 的键是**通道号字符串**;
- `status_hex` **不是 Redis 字段**:它由该板各通道的 `batt_status + pow_status`
(各 2 位 hex)按**通道号升序**拼回(`cabinet_tree::assemble_boards`);
- `channels` 里**只出现 Redis 中真实存在的通道键**(设备从未上报 / 键已失效的通道**整条省略**),
不会凭空多出"全空通道";
- 通道内 `on` / `full` / `fault` / `hex` / `status` 都是**由 `batt_status` / `pow_status` 派生**的响应字段
(Redis 里没有这些字段名),推导规则见 [`Redis 实时状态存储`](#redis-实时状态存储)。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### PUT /api/cabinets/{id}/work-mode
设置柜子充电工作模式。权限 `device:edit` + 组织归属校验。
**请求:** `{ "work_mode": "0" }`(**字符串**,前端契约)—— 白名单取值,`"0"`=平台指令模式(默认)、
`"1"`=自动充电模式;其它值(含 `"2"`/`"true"`)返回 400。
**响应:** `{ "code": 0, "data": { "success": true }, "message": "ok" }`
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
### GET /api/cabinets/{id}
设备详情(柜级 + 仓控板 + 仓体),权限 `device:view` + 组织归属校验(`scope_guard`)。
**关键字段与语义:**
| 字段 | 说明 |
|------|------|
| `status` | **柜子级**状态,**原样取自 `cabinets.status`**(`2` = 禁用,见下方 `PUT .../status`)。⚠️ **不是**派生值、**不是** `0/1/2/3` 档 |
| `status1` / `status2` | **柜级状态位**(协议 §3.8),2 位小写 hex **原样透出**(如 `"10"` = 柜门打开、`"82"` = 喷淋+烟雾);**取不到为 `null`,绝不落 `"00"`**(`"00"` 是"全部正常"的有效值,两者必须区分) |
| `temp` | 柜温,**K×100**(Redis `device:{imei}.temp`);取不到为 `null`(前端显示 `--°C`),不落 `0` |
| `rssi` / `pow_fail_dc` / `pow_fail_ac` | 取不到均为 `null` |
| `work_mode` | 字符串 `"0"` / `"1"`,无值时为 `null` |
| `cabin_boards[].compartments[].status` | **仓体级派生值** `0 空闲 / 1 充电中 / 2 充满 / 3 故障`(由通道 `batt_status` / `pow_status` 经 `derive_channel_flags` 推导,优先级 `fault > full > charging > idle`);**拿不到通道数据时保留 DB 占用值 `0`/`1`**;**没有"禁用"档** |
| `compartments[].batt_status` / `pow_status` | 2 位 hex,取不到为 `null` |
| `compartments[].soc` / `voltage` / `current` / `temp` | 单位同下表(‰ / mV / mA / K×100) |
> 🔎 **两个 `status` 不是一回事,前端必须分清**:
> 顶层的 `status` 是**柜子**的(0/1/2,含"禁用");`cabin_boards[].compartments[].status`
> 是**仓体**的派生展示值(0/1/2/3,**无"禁用"**)。
> **`cabinets.status` / `compartments.status` 两列在 DB 里都只表达"占用/启用"**,
> "充满"/"故障"**不落库**,永远由 Redis 通道状态实时派生。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### PUT /api/cabinets/{id}/status
禁用 / 启用柜子。权限 `device:edit` + 组织归属校验,**且仅总管理员(`role_level=2`)可执行**,
否则 403「仅总管理员可禁用/启用柜子」。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
**请求:** `{ "status": 2 }`(`2` = 禁用,**其它任何值都按"启用"处理**,
`routes/cabinets.rs::set_cabinet_status` 的 `if body.status == 2` 分支);
**响应:** `{ "code": 0, "data": null, "message": "ok" }`
> ⚠️ `cabinets.status = 2` 是**指令链路拦截**的判据:`h5.rs::ensure_cabinet_enabled`
> 与 `CABINET_DISABLED_SQL`(`SELECT COUNT(*) > 0 FROM cabinets WHERE id = ? AND status = 2`)
> 据此拒绝开合闸/开门/充电。
> ⚠️ **列注释已对齐(本轮修复)**:`db/migrate.rs` 里 `cabinets.status` 的注释原为
> `'0离线 1在线'`(**漏了 `2`**),代码里实际是 `0` 离线 / `1` 在线(`energy-stats/summary` 的
> `online_count` 即按 `status = 1` 计数)/ `2` 禁用;现已改为
> `'0离线 1在线 2禁用(禁用后指令链路拦截)'`,并由启动迁移幂等对齐(见下)。
> ⚠️ **在线的"权威判据"是 Redis `device:{imei}` 的 TTL 300s**(协议 §2.3),
> `cabinets.status` 只是库里的一个静态标记,两者不保证同步。
> 🔧 **注释口径的落库方式(`cabinets.status` 与 `charge_orders.status` 同一套)**:
> `migrate.rs::run()` 用 `ensure_column_comment(pool, 表, 列, marker, ddl)` **只在现有注释不含
> `marker` 时才 ALTER**(marker 分别是 `2禁用` / `保留未使用`),因此**重启不会每次做 DDL**;
> 老库(含生产库 `pms`)在**下一次启动新版 api-server 时自动对齐**。
> 实测:改动后首次启动打印 `[migrate] 更新列注释 ...`,再次启动即 `未执行任何 DDL`。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
### PUT /api/organizations/{id}/status
禁用 / 启用组织。**仅总管理员**(代码内 `role_level < 2` 判断,**不走权限码**);
禁用后该组织及其子组织的柜子全部不可用(指令链路拦截)。
**请求:** `{ "disabled": 1 }`(only `0`/`1`,其它值 400);**响应:** `{ "code": 0, "data": null, "message": "禁用成功" }`
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:40:46 +08:00
### GET /api/charge-records/export
异步导出充电记录 Excel。创建导出任务后自行去下载中心取文件。
**查询参数:** 同 `GET /api/charge-records`
**响应:**
```json
{ "code": 0, "data": { "task_id": 1 }, "message": "导出任务已创建" }
```
### GET /api/charge-orders
充电订单列表。订单记录一次充电事件,含电池ID、SOC变化、结束原因。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> 🔒 **组织隔离 + 数据范围(本轮新增)**:此前该接口**完全没有组织隔离**,任何登录用户都能看到
> 全部组织的订单。现在按 `charge_orders.cabinet_id → cabinets.project_id → projects.organization_id` 过滤:
> 工厂(`role_level >= 2`)全量;客户侧仅本组织;`users.organization_id` 为 NULL 的账号**返回空集**
> (SQL 里追加 `AND 1=0`,`routes/charge_orders.rs::order_scope_conditions`)。
> 组织条件之后再叠加**项目级数据范围**(`cabinet_id IN (SELECT id FROM cabinets WHERE project_id IN (...))`),
> 未配置范围时不产生额外条件。**列表与 `total` 计数走同一套条件**,不会出现"总数含不可见行"。
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:40:46 +08:00
**查询参数:**
| 参数 | 类型 | 说明 |
|------|------|------|
| `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 | 结束时间(含) |
**响应:**
```json
{
"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",
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
"start_soc": 200,
"end_soc": 950,
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:40:46 +08:00
"energy": 1200,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"reason": "normal",
"reason_text": "正常充满",
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:40:46 +08:00
"status": 1,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"status_text": "已完成",
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:40:46 +08:00
"created_at": "2026-07-05 10:00:05"
}
],
"total": 50,
"page": 1,
"page_size": 20
},
"message": "ok"
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
**订单状态(`status`):** `0` 充电中 / `1` 已完成(终态)。
⚠️ **只有 2 档**,且历史文档写的"0=充电中,1=正常结束,2=手动停止,3=异常终止,4=拔出结束"
是**错的**:`charge_orders.status` 列注释为 `0充电中 1已完成 2异常`,但**生产代码没有任何写入点会写
`2`** —— 所有结束路径(设备上报 `off`/`bat_out`、换电池、10 小时超时、无电池对账)都走
`report_worker::finalize_order*`,`order_status` 恒为 `ORDER_STATUS_DONE = 1`
(`report_worker.rs:2421`、调用点 `:2854` / `:2970` / `:2513`)。
唯一的 `status = 2` 出现在 **`test/reset_fixture.py:27`**(一次性夹具脚本,**非交付物**,连 `pms_dev`)。
> 📌 **`2` 未被删除,而是标注为"保留未使用"(本轮)**:`2` **有消费方** —— PC 订单页
> `web/src/pages/charge-orders/index.tsx` 的筛选下拉提供 `<Option value={2}>异常</Option>`、
> `STATUS_COLOR[2] = 'red'`,`routes/charge_orders.rs::status_text` 也把 `2` 映射成"异常"。
> 即"异常终止"曾被设计成一个**状态**,而现实现把它降级成 `reason` 里的**结束原因** ——
> 这是**产品语义缺口,不是笔误**,故列注释改为
> `'0充电中 1已完成 2异常(保留未使用:平台当前无写入方,结束原因见 reason)'`
> (保留 `2`、注明现状),并作为**待产品决策项**:
> 要么承认"异常"只是 `reason`(则后续可移除 UI 里的死筛选档),
> 要么补上写入方(真正把异常订单落成 `status = 2`)。
**结束原因在 `reason` / `reason_text` 里,不在 `status` 里**(`charge_orders.reason` 列注释
`normal/user_stop/forced/timeout`;`routes/charge_orders.rs::reason_text`):
| `reason` | `reason_text` | 触发场景 |
|---|---|---|
| `normal` | 正常充满 | 设备上报充满结束 |
| `user_stop` | 手动停止 | 平台下发 `off`(小程序/管理端停止充电) |
| `forced` | 强制结束 | 拔电池 / 异常中断等强制收尾 |
| `timeout` | 超时结束 | 订单 10 小时超时兜底结单 |
| 其它 / NULL | 未知 | — |
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:40:46 +08:00
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
**单位:** `energy` = **Wh**(整数);`start_soc` / `end_soc` = **‰**(整数,0–10000,前端 `/ 10` 显示 %)。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
`end_soc` 可为 `null`(订单未结束或收尾时取不到末帧 SOC)。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
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:40:46 +08:00
### GET /api/batteries
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
电池台账列表(`batteries` 表)。**每块电池一行**,记录 `battery_id` / 当前电量 / 状态;
插入 / 拔出事件本身在 `charge_orders`(订单)里体现,本表**不记录**事件流水。
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:40:46 +08:00
**查询参数:**
| 参数 | 类型 | 说明 |
|------|------|------|
| `page` | int | 页码(默认 1) |
| `page_size` | int | 每页条数(默认 20,最大 100) |
| `status` | int | 状态过滤 |
| `battery_id` | string | 电池ID(模糊匹配) |
**响应:**
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"battery_id": "BAT001",
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"soc": 4500,
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:40:46 +08:00
"brand": "星恒",
"model": "XH-48V",
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"sn": "SN123456",
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:40:46 +08:00
"status": 1,
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
"status_text": "使用中",
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:40:46 +08:00
"created_at": "2026-07-01 08:00:00"
}
],
"total": 50,
"page": 1,
"page_size": 20
},
"message": "ok"
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
**电池状态:** `0` 空闲 / `1` 使用中 / `2` 故障 / `3` 报废(`batteries` 表字段注释与
`batteries.rs::status_text` 一致;越界值返回 `"未知"`)。
**单位:** `soc` = **‰**(4500 = 45.00%,表字段注释即 `电量(‰)`)。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
⚠️ 历史文档曾把示例写成 `"soc": 45`,那是按"百分比"写的**错误示例**,实现返回的是千分比。
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> ⚠️ **本接口没有 `voltage` 字段**(历史文档的示例里有 `"voltage": 52000`,是**臆造**):
> `batteries` 表只有 `soc`,**没有任何电压列**(实测 `SHOW COLUMNS FROM batteries` 无 `voltage`,
> 只有 `rated_voltage` 额定电压且不在响应里)。
> 同样不存在的响应字段还有 `cabinet_id` / `compartment_id` / `last_seen` ——
> `BatteryItem` 的完整字段就是上例这 9 个(`routes/batteries.rs::BatteryItem`)。
> 需要电压/电流请走 `GET /api/compartments/{id}` 或设备详情的通道级实时字段。
> 🔒 **组织隔离 + 数据范围:走「间接归属」(2026-09 修复)**
>
> `batteries` 表**没有** `organization_id` / `project_id` / `cabinet_id`(也无外键、无关联列),
> 所以归属只能**间接推导**(`routes/batteries.rs::build_battery_filters`):
>
> ```
> batteries.battery_id → charge_orders.battery_id → charge_orders.cabinet_id
> → cabinets.project_id → projects.organization_id
> ```
>
> 落成 SQL 是在原过滤器**之前**无条件追加一个 `EXISTS` 子查询,外加数据范围条件:
> 总管理员(`role_level >= 2`)→ 不加归属条件(全量);
> 客户侧 → `EXISTS (SELECT 1 FROM charge_orders co JOIN cabinets c ... JOIN projects p ...
> WHERE co.battery_id = batteries.battery_id AND p.organization_id = ?)`,
> 并在**同一子查询**的 `c.project_id` 上叠加数据范围(未配置 = 不限制);
> `users.organization_id` 为 `NULL` → `1=0` **空集**(绝不退化成全量)。
>
> **这个归属口径的语义(必须按此理解,否则会误判"少数据"是 bug)**:
> - 判据是「**这块电池被本组织用过**」—— 依据是多笔 `charge_orders`
> (设备上报 `bat_in` / 平台建单时写入 `battery_id`);
> - ⚠️ **"用过即可见",不区分"谁的电池"**:同一块 `battery_id` 若被**多个组织**用过,
> 则这些组织**都能看到它**(归属是"多对多"的,不是独占);
> - ⚠️ **从未建过单的电池对客户不可见**:`batteries` 表有行、但 `charge_orders` 里没有任何
> 该 `battery_id` 的记录 ⇒ `EXISTS` 不成立 ⇒ **客户查不到**(**这是有意为之**:
> 无归属证据就不展示,宁可少给也不越权);总管理员不受此限。
> - `status` / `battery_id LIKE` 等过滤器**排在归属条件之后**(有单测钉住顺序:
> 归属 → 模糊 → 状态),避免绑定值错位。
>
> ⚠️ **修复前**该接口只做 `check_permission(&user, "device:view")` + `SELECT * FROM batteries WHERE 1=1`,
> **没有任何组织隔离**(全文无 `effective_scope` / `org_condition`)——
> 任何持 `device:view` 的客户账号都能看到**全部组织**的电池台账。现已修复如上。
feat(meter): 整柜日用电量统计接口(电表读数口径,与 energy_stats 严格分离) ## 背景 上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕 (`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。 ## ⛔ 与 energy_stats 的关系:两套口径,严禁混用 | | 数据源 | 口径 | |---|---|---| | **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) | | `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) | 电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致, **谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。 ## 计算口径 `当日用电量 = 当日最后一条读数 − 当日之前最近一条读数` 注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。 ## 三个语义坑(都处理了,都有单测) 1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。 渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。 2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒ 差值实际覆盖多天,直接标"当日用电"会高估。 3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量", 而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。 另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。 ## 汇总口径 `summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。 同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。 ## 为什么用 Rust 纯函数而不是全 SQL 算 三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。 这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给 `compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。 ## 接口 `GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天) 权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。 ## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删) 造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200): ``` 2026-03-01 end=1000 usage=None flags=['no_baseline'] 2026-03-02 end=1300 usage=300 flags=[] 2026-03-04 end=1500 usage=200 flags=['covers_multiple_days'] 2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced'] summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4 ⇒ 三个坑全部生效,03-03 缺席,汇总正确 ``` ## 判据自证能失败 撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上, 首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。 ## 验收 - `cargo check --all-targets`:**0 error / 0 warning**(两服务) - `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测) - web `npm run typecheck`:exit 0 - `device_status_parse` 16/16、`rssi_signal` 9/9 - 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP) - `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18** - 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94) - 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过) ## 文档 - `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数 - `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑 - `tasks/002`:标记日用电量统计已完成
2026-09-20 06:38:53 +08:00
### GET /api/cabinets/{id}/meter-usage
整柜**日用电量**(电表读数口径)。
> ⛔ **与 `energy-stats` 不是一回事,严禁混用**:
> - 本接口数据源是 `cabinet_meter_readings`(**电表读数**变化留痕),反映**整柜实际用电**
> (含非充电损耗,且是硬件计量);
> - `energy-stats` 数据源是 `charge_records`(订单结算),只反映**充电订单电量**
> (平台按 `V×I×Δt` 积分的**估算值**)。
>
> 两者数值必然不一致,谁也不替代谁。
**口径**:`当日用电量 = 当日最后一条读数 − 当日之前最近一条读数`
(注意是"当日**之前**最近一条",不是"当日第一条",否则当天早段的用电会被漏掉)。
**查询参数**(均为可选):
| 参数 | 说明 |
|------|------|
| `start` | 起始日期 `YYYY-MM-DD`(含);缺省 = 结束日期前 29 天 |
| `end` | 结束日期 `YYYY-MM-DD`(含);缺省 = 今天;`start`~`end` 最多 366 天 |
**响应:**
```json
{
"code": 0,
"data": {
"cabinet_id": 1,
"start": "2026-03-01",
"end": "2026-03-05",
"days": [
{ "day": "2026-03-01", "end_reading": 1000, "flags": ["no_baseline"] },
{ "day": "2026-03-02", "end_reading": 1300, "usage_wh": 300, "flags": [] },
{ "day": "2026-03-04", "end_reading": 1500, "usage_wh": 200, "flags": ["covers_multiple_days"] },
{ "day": "2026-03-05", "end_reading": 1200, "usage_wh": -300, "flags": ["meter_reset_or_replaced"] }
],
"summary": { "total_wh": 500, "counted_days": 2, "days_with_data": 4 }
},
"message": "ok"
}
```
**`flags`(数据质量标记)—— 前端必须据此决定怎么展示:**
| 值 | 含义 | 前端应怎么显示 |
|---|---|---|
| `no_baseline` | 该柜**第一条**读数,无对比基准 ⇒ `usage_wh` 字段**不出现** | 显示"无数据"(**不是 0 度**) |
| `covers_multiple_days` | 前值距当日首读超过 1 天(柜子离线了几天)⇒ 本行差值覆盖**多日** | 提示"覆盖 N 天",别当成单日用电 |
| `meter_reset_or_replaced` | 读数比前值**小**(换表 / 清零)⇒ 差值不是用电量 | 显示"电表更换/清零",**别显示负数用电量** |
**⚠️ 离线整天 = 无数据,不是 0**:当天一条读数都没有时,该日**根本不出现在 `days` 里**。
渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。
**汇总口径**:`summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)
与 `meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。
`counted_days` 是实际计入的天数,`days_with_data` 是有读数的天数(含被排除的)。
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:40:46 +08:00
### GET /api/energy-stats/summary
能耗概览,按组织数据隔离返回今日/本月能耗数据。
**响应:**
```json
{
"code": 0,
"data": {
"today_energy": 45000,
"month_energy": 1200000,
"estimated_cost": 840.00,
"cabinet_count": 36,
"online_count": 34
},
"message": "ok"
}
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
- `today_energy` / `month_energy` — **Wh**(整数)。数据源是**实时聚合 `charge_records`**
(按 `start_time` 的今日/本月窗口),**不是**旧实现读 `energy_stats.stat_date`;
- `estimated_cost` — 预估费用(**`f64`,单位元**,本文档唯一的非整数业务字段),
由 **`month_energy`** 按常量 **`COST_PER_WH_CENT`(分/Wh)** 计算:
`(month_energy * COST_PER_WH_CENT / 1000) as f64 / 100.0`
(`routes/energy_stats.rs::get_summary`;**不是** `COST_PER_WH`,旧文档常量名写错了);
- `cabinet_count` / `online_count` — 可见柜子总数 / 其中在线数(`cabinets.status = 1`,
且同样叠加**组织隔离 + 项目级数据范围**)。
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:40:46 +08:00
---
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
## 免认证接口与内部接口
> 本节全部来自 `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。
### 设备服务节点与指令接口(响应不含标准外层)
| 接口 | 方法 | 权限 | 响应 |
|------|------|------|------|
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `/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":[{node_id,ip,tcp_port}],"count":N}` |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `/api/devices/stats` | GET | `device:view` | `{"node_count":N}` |
| `/api/devices/{dev_id}/command` | POST | `device:operate` | `{"suc":1}` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> 🔒 **`/api/nodes/*` 于 2026-09 由"仅需登录"收紧为「仅总管理员」**(`routes/nodes.rs::require_super_admin`,
> 4 个处理器全部前置;判据是 `role_level >= SUPER_ADMIN_ROLE_LEVEL`(= `2`,`middleware/auth.rs:297`))——
> 客户侧(含**企业管理员 `role_level=1`**)返回 `403`。
> 收紧原因:这是**设备服务内部管理面** —— `GET /api/nodes` 会返回节点的**内网 `ip:tcp_port`**,
> 而 `POST /api/nodes/{node_id}/deregister` **真能把节点从注册表里删掉**(不是软标记)。
> ⚠️ **`device-server` 并不调用这些端点**(它启动时**直接写 Redis** `node:{node_id}`,
> 见 `device-server/src/main.rs`;且该写入含 `registered_at`,与 `nodes.rs::register()` 写的内容不同),
> 全仓(含前端)**没有任何调用方** —— 所以收紧**不影响设备上报链路**。
> 因此上表"设备服务节点注册"的调用方一栏也一并更正:**不是** device-server 调用。
>
> `devices/*` 才是管理端/后端调用。`nodes/*` 与 `devices/*` 都**不返回**
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> `{code,data,message}` 标准外层(见上表"内部接口例外")。
---
## 数据单位规范
**核心原则:所有数值字段使用整数存储,禁止浮点类型 (f32/f64/DECIMAL/FLOAT)**
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:40:46 +08:00
**单位转换仅在前端:** API 始终返回原始单位值,前端负责转换为用户友好单位显示。后端禁止做任何单位换算。
### 标量字段
| 字段 | 类型 | 单位 | 前端显示 | 转换公式 |
|------|------|------|----------|---------|
| `energy` / `total_energy` / `today_energy` / `month_energy` / `energy_kwh`¹ | i64 | **Wh** | kWh | `/ 1000` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `voltage` | i64 | **mV** | V | `/ 1000` |
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:40:46 +08:00
| `current` | i64 | **mA** | A | `/ 1000` |
| `soc` / `start_soc` / `end_soc` | i64 | **‰** | % | `/ 10` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `temp` / `max_temperature` | i64 | **K×100** | °C | `/ 100 - 273.15` |
| `meter` | i64 | **Wh**(电表读数) | kWh | `/ 1000` |
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:40:46 +08:00
| `rssi` | i64 | **0–99 信号等级** | 直接显示 | 不转换(不是 dBm) |
| `cost` / `estimated_cost` | i64/f64 | **元** | ¥ | 不转换(后端业务计算) |
| `today_charge_count` / `today_charge_hours` | i64 | 计数值 | 直接显示 | 不转换 |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `cell_voltages[]` / `cell_temps[]` | — | ⚠️ **恒为空数组**(协议未上报单体明细,无写入方) | — | — |
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:40:46 +08:00
¹ 注意:`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` |
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
| `125500` | 电表读数(Wh) | `125.5 kWh` |
| `54600` | 通道电压(mV) | `54.6 V` |
| `"82"` | 通道 `batt_status`(hex,bit7 有电池 + bit1 充电中) | 位域由前端纯函数解析 |
> ⚠️ 单体电压/温度的示例值(`[...]` 数组)**已删除**:`cell_voltages[]` / `cell_temps[]`
> 当前**恒为 `[]`**(协议未上报单体明细),写一个非空示例会误导前端按数组长度渲染。
---
## TCP 设备通讯协议
docs: 澄清"唯一 TCP 协议文档"并隔离硬件总线协议;删除已完成任务、摘出未结待办 ## 起因 用户提问:**"我们不是有一个 tcp 协议权威文档了吗?为啥还有其他 tcp 协议文档"** —— 查证后确认:**权威文档只有一份**,但目录布局让人容易误判,且**我上一轮的搬迁引入了一处断链**。 ## 一、查证结果:全仓只有一份 TCP 协议文档 按"是否同时出现 `auth_str`/`status_post`/`bat_in` 等 act 名"全仓扫描,含协议内容的只有: | 文件 | act 命中 | 性质 | |---|---:|---| | `docs/protocol/柜控板固件对接文档.md` | 171 | ⭐ **唯一权威 TCP 协议规格** | | `docs/tech/API.md` | 14 | HTTP 接口文档,TCP 章节**只有一句指回权威文档的链接** | | `AGENTS.md` / `数据模型.md` / `小程序用户端.md` | 13 / 8 / 3 | 规则与存储说明,非协议规格 | | `docs/archive/项目深度分析报告-2026-07-06.md` | 1 | 历史归档 | ⇒ **没有任何第二份 TCP 协议文档。** 用户看到的"另一份"应是下面二者之一(都已处理): ### 1. `docs/protocol/1TN协议标准_*` —— **不是 TCP 协议** 它是**仓控板 ↔ 翌工电源的 RS485 总线协议**(另一条物理链路)。与 TCP 权威文档同放 `protocol/`,名字又像"协议标准",极易被当成第二份平台协议。 **已移到新目录 `docs/hardware/`**(硬件侧总线协议),并在两处都写明"**不是**平台 TCP 协议"。 **结果:`docs/protocol/` 下有且仅有一个文件** —— 一眼就能确认"只有一份"。 ### 2. `tasks/001-TCP协议对齐与安全修复.md`(66 KB)—— 是任务记录,不是协议文档 文件名含"TCP协议",且 66 KB。它已被完整处理(见下节)。 ## 二、删除已完成的任务文件,并摘出真正的未结待办 `tasks/` **已 gitignore(`.gitignore:27`),是本地草稿区、从不入库**。 `tasks/001` 的 A~AH **全部 ✅ 已完成**,但其**状态头写着"待排期(未开始编码)"** —— 是错的。 按「已完成的任务文件及时清理」,**已删除**;删前把**唯一两项未结**摘成 `tasks/002-未结待办-协议待确认项与长函数重构.md`: 1. **`energy` 数据源待硬件答复**(协议待确认第 13 项,阻塞中)+ 关联的第 16 项; 2. **长函数重构**(已授权未开工)。 > ⚠️ **顺带纠正 001 的两处数字**(实测后才发现,**不能沿用**): > - 001 记"23 个函数超 80 行"—— **当前实测只有 8 个**(本会话多轮重构已拆掉大部分); > - 001 记 `migrate.rs::run` 已从 280 拆到 **46 行** —— **拆分本身没被回退** > (`SCHEMA_SQL` 确在 L20、`run` 内 0 个 `CREATE TABLE`),但后续每轮迁移改动都往 `run` 里追加 > (`LEGACY_COLUMN_TARGETS` 对齐、`ensure_active_order_unique`、列注释对齐……), > **它又长回 174 行,重新成为全仓最长函数** —— 这是一次**重构回归**,已列为第一优先。 > - 实测脚本随 002 一起留在 `tasks/`(该目录不入库,避免下次重新发明)。 ## 三、修复我上一轮搬迁引入的断链(1 处真缺陷 + 1 处机械替换误伤) 搬迁脚本只覆盖了 `docs/<name>` 与 `../<name>` 两种形态,**没覆盖三种情况**,已全部修好并加了体检: | 位置 | 问题 | 修复 | |---|---|---| | `docs/tech/API.md` | 同级相对链接 `](柜控板固件对接文档.md)` 因文件被移走而断链 | → `](../protocol/柜控板固件对接文档.md)` | | `docs/tech/数据模型.md` | 我的替换把 **markdown 链接目标** `](docs/API.md)` 也换成了 `docs/tech/API.md` ⇒ 解析成 `docs/tech/docs/tech/API.md` | 显示文字保留可读路径,**链接目标改回 `API.md`** | | `docs/archive/审查报告…md` | 指向当时已删除的 `./tcp-protocol.md`(历史事实) | 归档**保留原文**,顶部加**归档横幅 + 旧名→现名映射表** | **新增体检方法并实测能发现问题**:遍历 `docs/**/*.md` 的所有相对链接、逐个还原路径判存在性。 修完后复跑 → 只剩归档那一处(按设计保留),已用横幅给出映射。 > 📌 教训(已写进 `docs/README.md` 维护约定):**移动文档后必须做链接体检**, > 不能只做字符串替换 —— 同级相对链接、以及"写在链接目标里的仓库相对路径"都会被漏掉。 ## 四、文档地图同步更新 `docs/README.md` 新增: - 顶部**「关于到底有几份 TCP 协议文档」**专节:直接回答,并列出"三份内部件何时被删"的历史; - `hardware/` 目录说明; - `protocol/` 明确标注 **⛔ 不要在这里放第二份协议文档**(要补充就改那一份并升版本号)。 `AGENTS.md` 项目结构同步加 `hardware/` 一行。
2026-09-20 04:55:28 +08:00
> TCP 设备通讯协议见 **[docs/protocol/柜控板固件对接文档.md](../protocol/柜控板固件对接文档.md)**
> (**唯一权威**,v1.1.1)。**本文不再重复协议内容** —— 全仓只有这一份 TCP 协议规格文档。
---
## Redis 实时状态存储
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
> **这一节是平台内部实现契约**(不属于 HTTP 接口),权威来源是代码,不是本文档:
> `software/api-server/src/workers/report_worker.rs` **模块头注释**(第 13–26 行)+
> 写入点 `write_device_state` / `write_channel_state` / `save_channel_battery` / `handle_pow_fail`,
> 以及消费方 `routes/cabinet_tree.rs`、`routes/cabinets.rs`、`routes/h5.rs`。
> 接口只把这些 key 的字段**原样透出**(hex 透 hex、缺失透 `null`),位域解析在前后端各自的纯函数里做。
> ⚠️ **`AGENTS.md` 的「Redis 实时状态存储」一节仍是旧口径**(`hex` 4 字符 / `on`·`full`·`fault` /
> 板级 `status_hex`),以本节与上述代码为准。
### key 与字段(逐字段核对 `report_worker.rs`)
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
device:{imei} # 设备级 Hash,TTL 300s(= 在线判定窗口)
├─ online = "1" # 标记在线(任意上行都会刷新 TTL)
├─ rssi = "85" # 0–99 信号等级(不是 dBm)
├─ temp = "29800" # 柜温 K×100
├─ meter = "125500" # 电表读数 Wh(125.5 kWh)
├─ node_id = "node-1" # 设备连接所在节点(**device-server 写**,用于多节点指令路由)
├─ cabinet_status1 = "10" # 柜级状态位1,2 位小写 hex(取不到 → 不写字段)
├─ cabinet_status2 = "00" # 柜级状态位2,2 位小写 hex(取不到 → 不写字段)
├─ pow_fail_dc = "0"/"1" # 直流停电(pow_fail_dc 指令置 1;status1 bit0=0 时清 0)
└─ pow_fail_ac = "0"/"1" # 交流停电(同上)
device:{imei}:board:{板号}:ch:{通道号} # 通道级 Hash(板号/通道号**十进制不补零**,写入时不设 TTL)
├─ batt_status = "82" # 通道 18 字符 hex 的 byte0,2 位小写 hex
├─ pow_status = "02" # 通道 18 字符 hex 的 byte1(与电池状态相互独立)
├─ soc = "4500" # ‰
├─ voltage = "54600" # mV
├─ current = "0" # mA
└─ temp = "29800" # 电芯最高温 K×100
device:{imei}:board:{板号}:ch:{通道号}:bat # 最近一次 bat_in 的电池登记(内部键,TTL 48h)
├─ battery_id / soc / info # info = 18 字符 hex(与通道 hex 同布局)
├─ brand_code / model_code / material_code # 属性原值透传
└─ cell_count / rated_voltage / rated_capacity # 额定参数(mV / mAh)
device:{imei}:board:{板号}:ch:{通道号}:order # 订单电量累加器(内部键,energy.rs)
├─ mwh / maxtemp / ts / nobat # 毫瓦时累计 / 最高温 K×100 / 上次采样秒 / 连续无电池次数
```
fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对 - 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id 参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、 cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经 charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、 GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、 POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、 /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2; /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin - TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则 - 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式) - 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到") ⚠️ device-server 与 api-server 必须同批发布 - 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、 行号引用改小节引用、修 3 处错引小节 - 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2 与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、 charge_orders.status 标注 2 保留未使用) - 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红) - RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs - nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像 - 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级; docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正; 权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
**已废弃、代码里不存在的旧 key/字段**(旧文档曾记录,勿再引用):
- 板级 Hash `device:{imei}:board:{板号}` 及其 `status_hex` —— **新协议板级没有状态字节**;
`routes/cabinet_tree.rs` 的 `status_hex` 是**按通道 `batt_status + pow_status` 升序拼回来的响应字段**,
**不是 Redis key**;
- 通道 hash 里的 `on` / `full` / `fault` / `hex` —— **Redis 里没有这些字段**,
它们只作为**接口响应字段**由 `derive_channel_flags` 从 `batt_status` / `pow_status` 实时推导
(`hex` = 通道 18 字符 hex 的**前 4 个字符**,缺任一字节则为空串);
- 通道 hash 里的 `battery_id` / `brand` / `model` / `sn` —— 已迁到 `:bat` 内部键
(且品牌/型号存的是 **`brand_code` / `model_code` 原值**,"名称映射"用的 `brand` / `model` 列代码表未定,
目前为空);
- 通道 hex 的 **`"0001"`(4 字符)** —— 实际是 **18 字符 = 9 字节**
(`CHANNEL_HEX_LEN = 18`):byte0 电池状态 / byte1 电源状态 / byte2 SOC(1%) /
byte3–4 电压(0.01V, 大端) / byte5–6 电流(0.01A, 大端) / byte7–8 电芯最高温(1K, 大端);
平台换算为 ‰ / mV / mA / K×100(`parse_channel_hex`);
- **`cell_voltages` / `cell_temps` 单体明细** —— 协议未上报,**没有任何写入方**,
接口恒返回 `[]`(见 `GET /api/compartments/{id}`)。
### 协议 `bat_in` 的 `battery` 块(**不是**旧文档的"BMS 字段表")
协议 §5.5 的 `battery` 块 = **电池属性 + `info`(18 字符 hex)+ `cells`(电芯明细 hex)**:
| 字段 | 落库 / 落 Redis | 说明 |
|------|----------------|------|
| `battery_id` | `batteries.battery_id`、`:bat.battery_id` | 电池唯一标识,**可能为空串**(BMS 读不到序列号,不影响充电) |
| `info` | `:bat.info`(原样 hex) | **恰好 18 个 hex 字符**,布局与 `status_post.channels[]` 元素**完全相同**(§5.3) |
| `cells` | ❌ **不存** | 电芯明细整段 hex,长度 `cell_count × 4 + 16`;平台**当前不消费、不落库** |
| `brand_code` / `model_code` / `material_code` | `batteries` + `:bat` | 原值透传(`brand` / `model` 名称列待代码表) |
| `cell_count` / `rated_voltage` / `rated_capacity` | `batteries` + `:bat` | 节数 / 额定电压 mV / 额定容量 mAh |
> `:bat` 里的 `soc` 是**该次 `bat_in` 的起始 SOC(‰)**,供 `on` 建单取起始电量
> (协议里 `on` 报文**不带 `info`**);`bat_out` 时按 `battery_id` 比对后才清除登记。
### 队列与缓存 key(api-server / device-server 之间)
| key | 类型 | 说明 |
|-----|------|------|
| `device:reports` | LIST | 设备上报队列,device-server `LPUSH` → api-server `RPOP` **非阻塞轮询 + sleep 200ms** |
| `device:commands` | LIST | 下行指令队列,api-server `LPUSH` → device-server 消费 |
| `device:commands:dead` | LIST | 死信队列 |
| `pending:{imei}` | Hash(TTL 120s) | 已下发指令挂起项:`field = msg_id` / `value = act`,收到带 `suc` 的回执时按 **`msg_id` + `act` 双条件** `HDEL` |
| `auth_str:{imei}` | STRING | 安全码**缓存**(权威存储在 DB `cabinets.auth_str`,未命中回源并回填) |
| `auth_str:issued:{imei}` | STRING | 安全码已下发标记(下发后不可再次申领,须管理员重置) |
| `node:{node_id}` | Hash(TTL:device-server 用 `DEVICE_REGISTER_TTL`(默认 300s)、api-server 侧固定 300s,心跳刷新) | 节点注册信息:`node_id` / `ip` / `tcp_port` / `status` / `registered_at` / `last_heartbeat`;**两侧都写**(device-server 启动注册 + api-server 的 `/api/nodes/*`),`GET /api/nodes` 用 `SCAN node:*` 读取 |
### 读这一节时需要知道的实现细节
- **`cabinet_status1/2` 取不到时"不写字段"**(`HSET` 只在 `normalize_hex_byte` 得到合法 2 位 hex 时才带这两个参数),
接口侧把"字段不存在 / 空串 / 纯空白"统一判为 `null`,**绝不落 `"00"`**;
- **`pow_fail_dc` / `pow_fail_ac` 是一条指令一个字段**(`act` 即字段名,不再用 `info` 区分),
且 `status_post` 的 `cabinet.status1` **bit0 = 0(正常)时会被清成 `"0"`**,
bit0 = 1 时不动(避免抹掉刚置位的停电标记);
- 板号量程 **1–15**、通道号量程 **1–8**(协议 §3.7,**不是 0–7**);
越界的板/通道**整条跳过并记 WARN**,不会写出 `:board:0` / `:ch:9` 这类垃圾 key;
- 通道 hex 长度 ≠ 18 或含非 hex 字符 → 该通道**跳过**(记 WARN),不影响同板其它通道。
---
## 权限码列表
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> 以 `software/api-server/src/db/migrate.rs::seed_permissions` 的**种子表为准**(下表与之对齐,共 **31** 个权限码)。
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:40:46 +08:00
> ⚠️ 历史上代码里曾误用一个**不存在**的权限码 `device:door:unlock`(种子里从来没有它)→ 导致开门对所有角色 403,**已修正为 `charge:open_door`**。
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
> ⚠️ 同样地,**不存在** `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`。
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:40:46 +08:00
| 权限码 | 说明 |
|--------|------|
| `device:view` | 查看设备 |
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:40:46 +08:00
| `device:operate` | 操作设备(开合闸 / 重启 / 开门等柜级控制) |
| `device:create` | 添加设备 |
| `device:edit` | 编辑设备(含修改充电工作模式) |
| `device:delete` | 删除设备 |
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:40:46 +08:00
| `device:auth_str:reset` | 重置安全码(**仅总管理员**) |
| `charge:start` | 开始充电 |
| `charge:stop` | 停止充电 |
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:40:46 +08:00
| `charge:open_door` | 开门(柜门解锁) |
| `charge_record:view` | 查看充电记录 |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `charge_record:export` | 导出充电记录(**也用于下载中心 4 个接口**) |
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:40:46 +08:00
| `device_log:view` | 查看设备日志 |
| `energy:view` | 查看能耗 |
| `energy:export` | 导出能耗 |
| `user:view` / `user:create` / `user:edit` / `user:delete` | 用户管理 |
| `role:view` / `role:create` / `role:edit` / `role:delete` | 角色管理 |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `operation_log:view` | 查看操作日志(**也用于问题反馈列表/处理状态 2 个接口**) |
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:40:46 +08:00
| `org:view` / `org:create` / `org:edit` / `org:delete` | 组织管理 |
fix(ops): 恢复能力与运维缺口修复(建表顺序/备份/建单漏单/214仓体/队列上限/NOFILE) ## 恢复能力(此前实际为 0) - migrate.rs 的 SCHEMA_SQL 存在前向引用:charge_orders 建表引用其后才创建的 compartments → 全新空库必然报 MySQL 1824,而 main.rs 是 .expect("数据库迁移失败") + Restart=always → 无限崩溃循环。修正为把 cabin_boards/compartments 移到 cabinets 之后 (只挪位置、21 条语句字符数完全一致;实测旧版 20/21 → 新版 21/21, 并用真实二进制在全新空库跑通、建出 21 表 153 列) - 新增每日备份 deploy/backup-db.sh + deploy/pms-backup.cron: pms 与 pms_dev 各一份,--single-transaction --routines --triggers,单库 dump(无 USE/CREATE DATABASE, 演练可安全恢复到临时库),.part 原子 mv、gzip -t 校验、记录表数与精确行数、保留 7 天、flock 防重叠、 任一库失败则跳过清理并 exit 1(避免失败时误删旧备份)。cron 已真实触发验证; 恢复演练:21 表逐表行数一致(1339=1339)后清理无残留 - migrate.rs 每次启动无条件 ALTER cabinets.abstract_id 改为先查 information_schema 再决定, 实测老库触发 1 次、后续启动 0 次 ## 订单与占用 - h5.rs start_charge:校验顺序改为 权限→归属→设备启用→设备在线→建单→下发(原为先建单后校验, 设备离线时会留下幽灵单);取不到 bat_in 登记(48h TTL 过期)时不再跳过建单,改为按未知电池建单 (与设备侧 handle_device_on 兜底一致)—— 此前会导致整段会话无订单/无记录/无电量 - report_worker.rs 新增无订单却标占用的兜底复位(优先按通道 batt_status bit7,拿不到状态时按 超过 12 小时未更新),并已幂等清零生产历史遗留下的 214 行;两库复查均为 0 ## 队列与资源上限 - device-server:device:reports 加 LPUSH→LLEN→LTRIM 0 199999→LLEN 的 pipeline 与越界 WARN (此前无上限:68 柜停 8 天、1000 柜停 13 小时即打满 1GB Redis,且会连累同实例既有生产平台) - 死信队列加 LTRIM 0 9999 + EXPIRE 7 天,写入时 WARN 带累计死信数 - 两服务各加 600s 一次的队列深度 INFO 日志(可观测性) - 4 个 systemd 单元(仓库 + 线上已安装)加 LimitNOFILE=65535(原软限制 1024,约 1000 柜即 accept 失败) ## 文档 - docs/API.md:接口总表 71 → 79 条并与 mod.rs 的 79 次 .route( 注册双向比对一致; 修正 system:log(种子里不存在的权限码)→ operation_log:view、登出 GET→POST、能耗导出 → energy:export; 补齐 21 个未记录接口;新增"列表响应包装"与"关键数值字段单位"两节 ## 超长函数 - report_worker.rs handle_bat_out 100→63、ensure_charge_order 88→68;users.rs update_user 81→45 (纯重构:字符串字面量多重集零差异,allow/unwrap/expect/panic 计数不变) ## 验收 - cargo check 两服务 0 error/0 warning;cargo test api 90 + device 22 全过 - 前端真实类型检查 npm run typecheck(= tsc -b;原 `tsc --noEmit` 因 tsconfig 是 solution 文件而空跑=假绿) - 端到端联调(测试环境)13/13;生产已部署并验证:服务 active、health 200、ERROR 0、 消费链路正常、214 仓体清零、唯一索引在位
2026-09-20 01:21:17 +08:00
| `project:view` / `project:create` / `project:edit` / `project:delete` | 项目管理 |