Files
jsowell-charger-web/docs/plan/2026-06-13-ebike-auto-register-fallback-plan.md
2026-06-13 10:36:46 +08:00

377 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: 电单车自动建档兜底优化
## 背景
当前电单车设备正常自动建档依赖 `0x20 设备注册包`
实际现场已经发现部分设备只持续上报心跳日志,但没有发送注册包,导致平台能看到设备通信,却在 `pile_basic_info``pile_connector_info` 中查不到对应桩号和端口数据。后续扫码、下单、端口状态查询会因为基础数据缺失返回空或报错。
本方案目标是:在不放大误建脏数据风险的前提下,为“不发送注册包但会上报心跳”的电单车设备补一条安全的自动建档兜底链路。
## 当前问题
### 当前自动建档入口
- `jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/RegistrationHandler.java`
- 处理 `0x20 设备注册包`
- 解析 `EBikeMessageCmd20`
- 调用 `pileBasicInfoService.registrationEBikePile(message)`
- `jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java`
- `registrationEBikePile(EBikeMessageCmd20 message)` 会先按 `message.getPhysicalId()` 查询 `pile_basic_info`
- 查到则直接返回。
- 查不到则创建 `pile_basic_info`,并按注册包 `portNumber` 创建 `pile_connector_info`
### 当前缺口
- 如果设备不发送 `0x20``registrationEBikePile` 不会执行。
- `0x21 设备心跳包` 只更新端口状态,不会补建基础数据。
- `0x06 端口充电时功率心跳包` 只更新单端口状态和实时数据,不会补建基础数据。
- `updateConnectorStatus()` 对不存在的端口执行 update 时只会更新 0 行,不会报错,也不会自动创建。
- 当前自动建档 service 内部没有“开始创建 / 已存在 / 创建成功 / 创建失败”的明确日志,只能从 `设备注册包:{}` 间接判断是否进入过注册 handler。
### 数据可用性问题
现有自动建档逻辑默认写入:
- `merchant_id = 1`
- `station_id = 2`
- `model_id = null`
- `software_protocol = 3`
其中 `model_id = null` 有明显风险:部分查询端口详情的 SQL 会 inner join `pile_model_info`,即使 `pile_basic_info` 有记录,也可能因为没有型号导致详情查不到。
## 优化目标
1. 设备即使没有发送 `0x20` 注册包,只要上报 `0x21` 设备心跳包,也能兜底创建桩和端口基础数据。
2. 自动创建必须幂等,不能因为高频心跳或并发消息产生重复桩、重复端口。
3. 自动创建的数据必须可追溯,能从日志和数据库字段看出来源。
4. 自动创建只解决“基础数据不存在”的问题,不绕过站点、型号、计费模板等运营配置要求。
5. 优化后要能通过日志确认是否执行了兜底建档、创建了多少端口、是否因已存在而跳过。
## 优化方案
### 1. 抽取通用自动建档方法
`PileBasicInfoService` 中新增通用方法:
```java
void ensureEBikePileRegistered(String pileSn, int portNumber, String source);
```
建议含义:
- `pileSn`:电单车物理 ID 转换后的桩号。
- `portNumber`:设备端口总数。
- `source`:触发来源,例如 `registration_0x20``heartbeat_0x21`
将现有 `registrationEBikePile(EBikeMessageCmd20 message)` 改为调用该通用方法:
```java
ensureEBikePileRegistered(
message.getPhysicalId() + "",
message.getPortNumber(),
"registration_0x20"
);
```
### 2. 在 0x21 设备心跳包增加兜底建档
修改 `HeartbeatHandler.supplyProcess()`
```java
EBikeMessageCmd21 message = new EBikeMessageCmd21(dataProtocol.getBytes());
String pileSn = message.getPhysicalId() + "";
saveLastTimeAndCheckChannel(pileSn, ctx);
log.info("设备心跳包:{}", JSON.toJSONString(message));
pileBasicInfoService.ensureEBikePileRegistered(
pileSn,
message.getPortNumber(),
"heartbeat_0x21"
);
updatePileStatus(message);
```
选择 `0x21` 作为主兜底入口的原因:
- `0x21` 能证明设备真实在线并正在与平台通信。
- `0x21` 包含 `physicalId``portNumber`,足够创建桩和全量端口。
- 相比扫码/下单接口,心跳包更接近设备事实,误建概率更低。
### 3. 不建议在扫码或下单接口直接兜底创建
扫码或下单接口通常只能拿到二维码参数或桩号,缺少端口总数、设备在线状态和协议上下文。
如果在这些入口自动创建,容易出现:
- 用户扫错码也创建数据。
- 伪造请求创建脏桩号。
- 端口数量无法判断,只能猜。
- 新建数据缺少型号、计费模板、站点归属,仍无法稳定下单。
因此扫码/下单入口只建议保留“查不到时给出明确错误或提示”,不承担建档。
### 4. 0x06 功率心跳只做二级兜底
`0x06` 只有当前端口号,不知道设备总端口数,不适合作为完整建桩依据。
建议处理策略:
- 如果 `pile_basic_info` 不存在:只打印 warn 日志,不自动创建完整桩。
- 如果 `pile_basic_info` 存在但当前 `pile_connector_code` 不存在:可补建这个单端口,并打印 warn 日志。
- 如果端口存在:按现有逻辑更新状态和实时数据。
这样可以减少充电中单端口状态丢失,但不把 `0x06` 作为主建档来源。
### 5. 默认归属和型号改为配置化
新增配置,避免继续在代码里硬编码:
```yaml
ebike:
auto-register:
enabled: true
merchant-id: 1
station-id: 2
model-id: 0
```
说明:
- `enabled`:兜底开关,便于灰度和回滚。
- `merchant-id`:自动建档默认运营商。
- `station-id`:自动建档默认站点,建议使用“待配置设备站点”。
- `model-id`:默认电单车型号,必须配置成有效 `pile_model_info.id`
不建议让 `model-id` 为空。否则后续查询端口详情时可能因为 inner join 型号表导致仍然查不到。
### 6. 自动建档数据加可追溯标记
建议自动创建时设置:
- `create_by = system`
- `remark = 自动建档:<source>; portNumber=<n>`
- 端口 `create_by = system`
如需更清晰,也可以后续增加独立字段,例如 `auto_register_flag``auto_register_source`,但这涉及数据库结构变更,本轮可以先用 `remark` 兜住。
### 7. 幂等和并发控制
心跳是高频数据,必须防止并发重复插入。
建议双层保护:
1. 应用层 Redis 锁:
```text
ebike:auto_register:pile:{pileSn}
```
- 使用 `setnx`
- 锁过期时间建议 10-30 秒。
- 拿不到锁时直接跳过或短路查询。
2. 数据库唯一约束:
- `pile_basic_info.sn` 建议保证唯一。
- `pile_connector_info.pile_connector_code` 建议保证唯一。
如果当前线上表没有唯一索引,至少在代码里要做到先查再插,并捕获重复键异常;中长期建议补唯一约束。
### 8. 端口补齐策略
`ensureEBikePileRegistered` 不只处理“桩不存在”,也要处理“桩存在但端口缺失”。
建议规则:
- 桩不存在:创建桩,并创建 `01``portNumber` 的端口。
- 桩存在:查询已有端口,只补缺失端口。
- 如果后续心跳 `portNumber` 小于已有端口数:不删除端口,只打印 warn。
- 如果后续心跳 `portNumber` 大于已有端口数:补齐新增端口。
这样可以兼容设备端口数变化,也避免误删历史数据。
### 9. 补齐日志
建议在通用建档方法中新增统一日志关键词,便于线上排查:
```text
电单车自动建档-开始, pileSn:{}, portNumber:{}, source:{}
电单车自动建档-已存在, pileSn:{}, source:{}
电单车自动建档-创建成功, pileSn:{}, connectorCount:{}, source:{}
电单车自动建档-补齐端口, pileSn:{}, missingConnectors:{}, source:{}
电单车自动建档-跳过, pileSn:{}, reason:{}, source:{}
电单车自动建档-失败, pileSn:{}, source:{}
```
同时建议 `updateConnectorStatus()` 在 update 结果为 0 时打印 warn
```text
更新枪口状态-未匹配到端口, pileConnectorCode:{}, status:{}
```
这样可以快速区分“设备有心跳但端口未建档”和“状态正常更新”。
## 需要修改的代码
### `jsowell-pile/src/main/java/com/jsowell/pile/service/PileBasicInfoService.java`
新增通用兜底建档接口:
- `ensureEBikePileRegistered(String pileSn, int portNumber, String source)`
保留现有:
- `registrationEBikePile(EBikeMessageCmd20 message)`
### `jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java`
主要改动:
- 抽取 `ensureEBikePileRegistered`
- `registrationEBikePile` 内部改为调用通用方法。
- 增加 Redis 锁。
- 增加默认运营商、默认站点、默认型号配置读取。
- 支持桩存在时补齐缺失端口。
- 增加自动建档日志。
- 建档或补端口后清理相关缓存。
### `jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/RegistrationHandler.java`
主要改动:
- 保留原有注册包处理。
- 调用通用建档方法时传 `source = registration_0x20`
- 保留现有 `设备注册包:{}` 日志。
### `jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/HeartbeatHandler.java`
主要改动:
-`updatePileStatus(message)` 前调用 `ensureEBikePileRegistered`
-`source = heartbeat_0x21`
- 如果兜底建档失败,需要打印 error但不要影响设备心跳应答避免设备因平台异常断链。
### `jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/PowerHeartbeatHandler.java`
可选改动:
- `0x06` 不作为完整建桩入口。
- 如果状态更新返回 0可打印更明确日志。
- 如果基础桩存在但端口不存在,可调用端口补齐方法或复用 `ensureEBikePileRegistered` 的端口补齐能力。
### `jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java`
建议改动:
- `updateConnectorStatus()` 获取 mapper update 返回值后,如果为 0打印 warn。
- 保持返回值向上透出,方便 handler 判断是否需要兜底。
### `jsowell-admin/src/main/resources/application-*.yml`
建议新增配置:
- `ebike.auto-register.enabled`
- `ebike.auto-register.merchant-id`
- `ebike.auto-register.station-id`
- `ebike.auto-register.model-id`
不同环境可以使用不同默认站点和型号。
## 可能产生的后果
### 正向影响
- 不发送注册包但发送设备心跳的电单车,也能自动生成基础桩和端口数据。
- 心跳状态更新不再因为端口不存在而长期更新 0 行。
- 排查能力增强,可以从日志确认是否执行了自动建档和端口补齐。
- 减少“设备在线但后台查不到桩”的问题。
### 业务风险
- 自动创建的数据默认归属到配置站点和运营商,可能不是最终真实归属。
- 如果默认型号配置错误,可能导致端口类型、详情查询、计费判断异常。
- 自动建档不等于可运营;仍需要计费模板、站点开放状态、运营商归属等配置完整。
- 未配置设备可能进入后台列表,需要运营人员识别和处理。
### 数据风险
- 如果没有唯一约束,高并发心跳下可能重复插入。
- 设备上报的 `portNumber` 如果异常,可能创建过多端口。
- `0x21` 端口数变化时,只补不删,可能留下历史端口,需要人工核对。
### 安全风险
- 任何能连上电单车 netty 端口并伪造合法协议包的设备,都可能触发自动建档。
- 需要通过网络准入、桩号范围校验、默认待配置站点、后台审核等方式降低风险。
- 如果后续支持设备白名单,自动建档前应先校验白名单。
### 性能风险
- 心跳高频进入后,每次都查库会增加压力。
- 需要使用 Redis 锁和短期缓存降低重复建档查询。
- 日志要控制频率,避免每次心跳都打印“已存在”导致日志量过大。
## 验收方案
### 场景 1新设备只发 0x21不发 0x20
预期:
- 日志出现 `电单车自动建档-开始`
- `pile_basic_info` 生成对应 `sn`
- `pile_connector_info` 生成 `01``portNumber` 的端口。
- 随后的心跳状态能正常更新端口。
### 场景 2设备正常发送 0x20
预期:
- 原有注册包建档逻辑不受影响。
- 日志 source 为 `registration_0x20`
- 重复注册包不会创建重复数据。
### 场景 3桩存在但端口缺失
预期:
- `ensureEBikePileRegistered` 只补缺失端口。
- 已存在端口不重复插入。
- 日志打印缺失端口列表。
### 场景 4并发心跳
预期:
- Redis 锁生效。
- 只插入一条桩记录。
- 端口不重复。
### 场景 5默认配置缺失或关闭自动建档
预期:
- `enabled = false` 时不自动创建,只打印跳过原因。
- `model-id` 未配置或无效时不创建可运营数据,打印 error 或 warn。
## 推荐实施顺序
1. 先补日志:让现有 `0x20``0x21`、状态 update 0 行可观测。
2. 抽取 `ensureEBikePileRegistered`,让 `0x20` 走新方法。
3. 加配置项和 Redis 锁。
4. 接入 `0x21` 兜底建档。
5. 增加端口补齐逻辑。
6. 小范围上线,观察自动建档日志和新增数据。
7. 再考虑 `0x06` 单端口补齐。
## 不建议本轮做的事情
- 不建议在扫码接口自动创建桩。
- 不建议在下单接口自动创建桩。
- 不建议 `model_id` 继续默认写 `null`
- 不建议收到端口数变小后自动删除端口。
- 不建议没有开关、没有日志、没有锁就直接让心跳自动插库。