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

13 KiB
Raw Blame History

Plan: 电单车自动建档兜底优化

背景

当前电单车设备正常自动建档依赖 0x20 设备注册包

实际现场已经发现部分设备只持续上报心跳日志,但没有发送注册包,导致平台能看到设备通信,却在 pile_basic_infopile_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

当前缺口

  • 如果设备不发送 0x20registrationEBikePile 不会执行。
  • 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 中新增通用方法:

void ensureEBikePileRegistered(String pileSn, int portNumber, String source);

建议含义:

  • pileSn:电单车物理 ID 转换后的桩号。
  • portNumber:设备端口总数。
  • source:触发来源,例如 registration_0x20heartbeat_0x21

将现有 registrationEBikePile(EBikeMessageCmd20 message) 改为调用该通用方法:

ensureEBikePileRegistered(
    message.getPhysicalId() + "",
    message.getPortNumber(),
    "registration_0x20"
);

2. 在 0x21 设备心跳包增加兜底建档

修改 HeartbeatHandler.supplyProcess()

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 包含 physicalIdportNumber,足够创建桩和全量端口。
  • 相比扫码/下单接口,心跳包更接近设备事实,误建概率更低。

3. 不建议在扫码或下单接口直接兜底创建

扫码或下单接口通常只能拿到二维码参数或桩号,缺少端口总数、设备在线状态和协议上下文。

如果在这些入口自动创建,容易出现:

  • 用户扫错码也创建数据。
  • 伪造请求创建脏桩号。
  • 端口数量无法判断,只能猜。
  • 新建数据缺少型号、计费模板、站点归属,仍无法稳定下单。

因此扫码/下单入口只建议保留“查不到时给出明确错误或提示”,不承担建档。

4. 0x06 功率心跳只做二级兜底

0x06 只有当前端口号,不知道设备总端口数,不适合作为完整建桩依据。

建议处理策略:

  • 如果 pile_basic_info 不存在:只打印 warn 日志,不自动创建完整桩。
  • 如果 pile_basic_info 存在但当前 pile_connector_code 不存在:可补建这个单端口,并打印 warn 日志。
  • 如果端口存在:按现有逻辑更新状态和实时数据。

这样可以减少充电中单端口状态丢失,但不把 0x06 作为主建档来源。

5. 默认归属和型号改为配置化

新增配置,避免继续在代码里硬编码:

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_flagauto_register_source,但这涉及数据库结构变更,本轮可以先用 remark 兜住。

7. 幂等和并发控制

心跳是高频数据,必须防止并发重复插入。

建议双层保护:

  1. 应用层 Redis 锁:
ebike:auto_register:pile:{pileSn}
  • 使用 setnx
  • 锁过期时间建议 10-30 秒。
  • 拿不到锁时直接跳过或短路查询。
  1. 数据库唯一约束:
  • pile_basic_info.sn 建议保证唯一。
  • pile_connector_info.pile_connector_code 建议保证唯一。

如果当前线上表没有唯一索引,至少在代码里要做到先查再插,并捕获重复键异常;中长期建议补唯一约束。

8. 端口补齐策略

ensureEBikePileRegistered 不只处理“桩不存在”,也要处理“桩存在但端口缺失”。

建议规则:

  • 桩不存在:创建桩,并创建 01portNumber 的端口。
  • 桩存在:查询已有端口,只补缺失端口。
  • 如果后续心跳 portNumber 小于已有端口数:不删除端口,只打印 warn。
  • 如果后续心跳 portNumber 大于已有端口数:补齐新增端口。

这样可以兼容设备端口数变化,也避免误删历史数据。

9. 补齐日志

建议在通用建档方法中新增统一日志关键词,便于线上排查:

电单车自动建档-开始, pileSn:{}, portNumber:{}, source:{}
电单车自动建档-已存在, pileSn:{}, source:{}
电单车自动建档-创建成功, pileSn:{}, connectorCount:{}, source:{}
电单车自动建档-补齐端口, pileSn:{}, missingConnectors:{}, source:{}
电单车自动建档-跳过, pileSn:{}, reason:{}, source:{}
电单车自动建档-失败, pileSn:{}, source:{}

同时建议 updateConnectorStatus() 在 update 结果为 0 时打印 warn

更新枪口状态-未匹配到端口, 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 生成 01portNumber 的端口。
  • 随后的心跳状态能正常更新端口。

场景 2设备正常发送 0x20

预期:

  • 原有注册包建档逻辑不受影响。
  • 日志 source 为 registration_0x20
  • 重复注册包不会创建重复数据。

场景 3桩存在但端口缺失

预期:

  • ensureEBikePileRegistered 只补缺失端口。
  • 已存在端口不重复插入。
  • 日志打印缺失端口列表。

场景 4并发心跳

预期:

  • Redis 锁生效。
  • 只插入一条桩记录。
  • 端口不重复。

场景 5默认配置缺失或关闭自动建档

预期:

  • enabled = false 时不自动创建,只打印跳过原因。
  • model-id 未配置或无效时不创建可运营数据,打印 error 或 warn。

推荐实施顺序

  1. 先补日志:让现有 0x200x21、状态 update 0 行可观测。
  2. 抽取 ensureEBikePileRegistered,让 0x20 走新方法。
  3. 加配置项和 Redis 锁。
  4. 接入 0x21 兜底建档。
  5. 增加端口补齐逻辑。
  6. 小范围上线,观察自动建档日志和新增数据。
  7. 再考虑 0x06 单端口补齐。

不建议本轮做的事情

  • 不建议在扫码接口自动创建桩。
  • 不建议在下单接口自动创建桩。
  • 不建议 model_id 继续默认写 null
  • 不建议收到端口数变小后自动删除端口。
  • 不建议没有开关、没有日志、没有锁就直接让心跳自动插库。