From d7e8562f42f136ff645b56d12be4693026a95bc2 Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 13 Jun 2026 10:36:46 +0800 Subject: [PATCH] bugfix ebike auto register fallback --- ...06-13-ebike-auto-register-fallback-plan.md | 376 ++++++++++++++++++ .../src/main/resources/application.yml | 13 +- .../electricbicycles/HeartbeatHandler.java | 8 +- .../pile/service/PileBasicInfoService.java | 10 +- .../impl/PileBasicInfoServiceImpl.java | 199 +++++++-- .../impl/PileConnectorInfoServiceImpl.java | 4 + 6 files changed, 564 insertions(+), 46 deletions(-) create mode 100644 docs/plan/2026-06-13-ebike-auto-register-fallback-plan.md diff --git a/docs/plan/2026-06-13-ebike-auto-register-fallback-plan.md b/docs/plan/2026-06-13-ebike-auto-register-fallback-plan.md new file mode 100644 index 000000000..660f495ec --- /dev/null +++ b/docs/plan/2026-06-13-ebike-auto-register-fallback-plan.md @@ -0,0 +1,376 @@ +# 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 = 自动建档:; portNumber=` +- 端口 `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`。 +- 不建议收到端口数变小后自动删除端口。 +- 不建议没有开关、没有日志、没有锁就直接让心跳自动插库。 diff --git a/jsowell-admin/src/main/resources/application.yml b/jsowell-admin/src/main/resources/application.yml index b158f0628..65159b138 100644 --- a/jsowell-admin/src/main/resources/application.yml +++ b/jsowell-admin/src/main/resources/application.yml @@ -103,6 +103,17 @@ pagehelper: supportMethodsArguments: true params: count=countSql +# 电单车自动建档配置 +ebike: + auto-register: + # 设备未上报注册包时,允许通过0x21心跳兜底创建桩和端口基础数据 + enabled: true + # 默认归属信息建议配置为“待配置设备”运营商/站点,后续由后台人工调整 + merchant-id: 1 + station-id: 2 + # 必须按环境配置为有效的电单车型号ID;0表示未配置,仍会创建基础数据但详情查询可能受影响 + model-id: 0 + # 防止XSS攻击 xss: # 过滤开关 @@ -202,4 +213,4 @@ sms: # 是否启用代理 默认关闭 需手动开启 enable: false host: 127.0.0.1 - port: 8080 \ No newline at end of file + port: 8080 diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/HeartbeatHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/HeartbeatHandler.java index b9e876091..3736e4c5b 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/HeartbeatHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/electricbicycles/HeartbeatHandler.java @@ -47,9 +47,15 @@ public class HeartbeatHandler extends AbstractEBikeHandler { public byte[] supplyProcess(EBikeDataProtocol dataProtocol, ChannelHandlerContext ctx) { // 解析字节数组 EBikeMessageCmd21 eBikeMessageCmd21 = new EBikeMessageCmd21(dataProtocol.getBytes()); + String pileSn = eBikeMessageCmd21.getPhysicalId() + ""; // 保存时间 - saveLastTimeAndCheckChannel(eBikeMessageCmd21.getPhysicalId() + "", ctx); + saveLastTimeAndCheckChannel(pileSn, ctx); log.info("设备心跳包:{}", JSON.toJSONString(eBikeMessageCmd21)); + try { + pileBasicInfoService.ensureEBikePileRegistered(pileSn, eBikeMessageCmd21.getPortNumber(), "heartbeat_0x21"); + } catch (Exception e) { + log.error("电单车心跳兜底建档失败, pileSn:{}, portNumber:{}", pileSn, eBikeMessageCmd21.getPortNumber(), e); + } // 更新充电桩状态 updatePileStatus(eBikeMessageCmd21); return getResult(dataProtocol, Constants.zeroByteArray); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/PileBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/PileBasicInfoService.java index 5f069c91a..394c54f36 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/PileBasicInfoService.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/PileBasicInfoService.java @@ -248,7 +248,15 @@ public interface PileBasicInfoService { * 注册电单车桩 * @param message */ - void registrationEBikePile(EBikeMessageCmd20 message); + void registrationEBikePile(EBikeMessageCmd20 message); + + /** + * 确保电单车桩基础数据已注册 + * @param pileSn 桩号 + * @param portNumber 端口数量 + * @param source 触发来源 + */ + void ensureEBikePileRegistered(String pileSn, int portNumber, String source); List getPileDetailInfoList(String stationId); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java index 0b276b0fe..fc466724c 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java @@ -64,6 +64,16 @@ import java.util.stream.Collectors; @Slf4j @Service public class PileBasicInfoServiceImpl implements PileBasicInfoService { + private static final String EBIKE_AUTO_REGISTER_LOCK_KEY = "ebike:auto_register:pile:"; + + private static final String EBIKE_AUTO_REGISTER_CHECKED_KEY = "ebike:auto_register:checked:"; + + private static final int EBIKE_AUTO_REGISTER_LOCK_EXPIRE_SECONDS = 30; + + private static final int EBIKE_AUTO_REGISTER_CHECKED_EXPIRE_SECONDS = 300; + + private static final int EBIKE_AUTO_REGISTER_MAX_PORT_NUMBER = 64; + @Autowired private PileBasicInfoMapper pileBasicInfoMapper; @@ -94,6 +104,18 @@ public class PileBasicInfoServiceImpl implements PileBasicInfoService { @Value("${baseurl.prefix}") private String BASE_URL_PREFIX; + @Value("${ebike.auto-register.enabled:true}") + private Boolean ebikeAutoRegisterEnabled; + + @Value("${ebike.auto-register.merchant-id:1}") + private Long ebikeAutoRegisterMerchantId; + + @Value("${ebike.auto-register.station-id:2}") + private Long ebikeAutoRegisterStationId; + + @Value("${ebike.auto-register.model-id:0}") + private Long ebikeAutoRegisterModelId; + @Autowired private PileReservationInfoService pileReservationInfoService; @@ -1587,53 +1609,144 @@ public class PileBasicInfoServiceImpl implements PileBasicInfoService { */ @Override public void registrationEBikePile(EBikeMessageCmd20 message) { - // 根据物理id(桩编号)查询桩信息 - PileBasicInfo pileBasicInfo = this.selectPileBasicInfoBySN(message.getPhysicalId() + ""); - if (pileBasicInfo != null) { + ensureEBikePileRegistered(message.getPhysicalId() + "", message.getPortNumber(), "registration_0x20"); + } + + /** + * 确保电单车桩基础数据已注册。用于注册包和心跳包兜底建档。 + */ + @Override + public void ensureEBikePileRegistered(String pileSn, int portNumber, String source) { + if (!Boolean.TRUE.equals(ebikeAutoRegisterEnabled)) { return; } - List basicInfoList = Lists.newArrayList(); - List connectorInfoList = Lists.newArrayList(); - - // 组装pile_basic_info表数据 - PileBasicInfo basicInfo = new PileBasicInfo(); - // 桩编号 - String sn = message.getPhysicalId() + ""; - basicInfo.setSn(sn); - basicInfo.setBusinessType(Constants.ONE); // 经营类型 1-运营桩;2-个人桩 - basicInfo.setSoftwareProtocol(Constants.THREE); // 软件协议 - basicInfo.setMerchantId(Long.valueOf("1")); // 运营商id 默认1 - basicInfo.setStationId(Long.valueOf("2")); // 站点id - basicInfo.setModelId(null); // 型号id - basicInfo.setProductionDate(new Date()); // 生产日期 - basicInfo.setLicenceId(null); // TODO 证书编号 - basicInfo.setSimId(null); // TODO sim卡 - basicInfo.setRemark(null); // 备注 - basicInfo.setCreateBy(Constants.SYSTEM); // 创建人 - basicInfo.setDelFlag(DelFlagEnum.NORMAL.getValue()); // 删除标识 - basicInfoList.add(basicInfo); - - int portNumber = message.getPortNumber(); - PileConnectorInfo connectorInfo; - for (int i = 1; i < portNumber + 1; i++) { - // 组装pile_connector_info表数据 - connectorInfo = new PileConnectorInfo(); - connectorInfo.setPileSn(sn); // sn号 - String connectorCode = String.format("%1$02d", i); - connectorInfo.setPileConnectorCode(sn + connectorCode); // 枪口号 - connectorInfo.setStatus(Constants.ZERO); //状态,默认 0-离网 - connectorInfo.setCreateBy(Constants.SYSTEM); // 创建人 - connectorInfo.setDelFlag(DelFlagEnum.NORMAL.getValue()); // 删除标识 - connectorInfoList.add(connectorInfo); + if (StringUtils.isBlank(pileSn)) { + log.warn("电单车自动建档-跳过, pileSn:{}, reason:桩号为空, source:{}", pileSn, source); + return; + } + if (portNumber <= 0 || portNumber > EBIKE_AUTO_REGISTER_MAX_PORT_NUMBER) { + log.warn("电单车自动建档-跳过, pileSn:{}, portNumber:{}, reason:端口数量异常, source:{}", + pileSn, portNumber, source); + return; } - // 批量入库 - PileTransactionDTO transactionDTO = PileTransactionDTO.builder() - .pileBasicInfoList(basicInfoList) - .pileConnectorInfoList(connectorInfoList) - .build(); - pileTransactionService.doCreatePileTransaction(transactionDTO); - } + String checkedKey = EBIKE_AUTO_REGISTER_CHECKED_KEY + pileSn; + String checked = redisCache.getCacheObject(checkedKey); + if (StringUtils.equals(Constants.ONE, checked)) { + return; + } + + String lockKey = EBIKE_AUTO_REGISTER_LOCK_KEY + pileSn; + Boolean locked = redisCache.setnx(lockKey, source, EBIKE_AUTO_REGISTER_LOCK_EXPIRE_SECONDS); + if (!Boolean.TRUE.equals(locked)) { + log.debug("电单车自动建档-跳过, pileSn:{}, reason:未获取到锁, source:{}", pileSn, source); + return; + } + + try { + checked = redisCache.getCacheObject(checkedKey); + if (StringUtils.equals(Constants.ONE, checked)) { + return; + } + log.info("电单车自动建档-开始, pileSn:{}, portNumber:{}, source:{}", pileSn, portNumber, source); + + PileBasicInfo pileBasicInfo = this.selectPileBasicInfoBySN(pileSn); + List connectorInfoList = pileConnectorInfoService.selectPileConnectorInfoList(pileSn); + List missingConnectorInfoList = buildMissingEBikeConnectorInfoList(pileSn, portNumber, connectorInfoList); + + if (pileBasicInfo == null) { + PileBasicInfo basicInfo = buildAutoRegisterEBikeBasicInfo(pileSn, portNumber, source); + PileTransactionDTO transactionDTO = PileTransactionDTO.builder() + .pileBasicInfoList(Lists.newArrayList(basicInfo)) + .pileConnectorInfoList(missingConnectorInfoList) + .build(); + pileTransactionService.doCreatePileTransaction(transactionDTO); + cleanEBikeAutoRegisterCache(pileSn, basicInfo.getStationId()); + log.info("电单车自动建档-创建成功, pileSn:{}, connectorCount:{}, source:{}", + pileSn, missingConnectorInfoList.size(), source); + } else { + if (CollectionUtils.isNotEmpty(missingConnectorInfoList)) { + pileConnectorInfoService.batchInsertConnectorInfo(missingConnectorInfoList); + cleanEBikeAutoRegisterCache(pileSn, pileBasicInfo.getStationId()); + log.info("电单车自动建档-补齐端口, pileSn:{}, missingConnectors:{}, source:{}", + pileSn, missingConnectorInfoList.stream().map(PileConnectorInfo::getPileConnectorCode).collect(Collectors.toList()), source); + } else { + log.info("电单车自动建档-已存在, pileSn:{}, portNumber:{}, source:{}", pileSn, portNumber, source); + } + if (CollectionUtils.isNotEmpty(connectorInfoList) && connectorInfoList.size() > portNumber) { + log.warn("电单车自动建档-端口数量变小, pileSn:{}, dbConnectorCount:{}, reportedPortNumber:{}, source:{}", + pileSn, connectorInfoList.size(), portNumber, source); + } + } + + redisCache.setCacheObject(checkedKey, Constants.ONE, EBIKE_AUTO_REGISTER_CHECKED_EXPIRE_SECONDS); + } catch (Exception e) { + log.error("电单车自动建档-失败, pileSn:{}, portNumber:{}, source:{}", pileSn, portNumber, source, e); + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } finally { + redisCache.deleteObject(lockKey); + } + } + + private PileBasicInfo buildAutoRegisterEBikeBasicInfo(String pileSn, int portNumber, String source) { + PileBasicInfo basicInfo = new PileBasicInfo(); + basicInfo.setSn(pileSn); + basicInfo.setBusinessType(Constants.ONE); + basicInfo.setSoftwareProtocol(Constants.THREE); + basicInfo.setMerchantId(ebikeAutoRegisterMerchantId); + basicInfo.setStationId(ebikeAutoRegisterStationId); + if (ebikeAutoRegisterModelId != null && ebikeAutoRegisterModelId > 0) { + basicInfo.setModelId(ebikeAutoRegisterModelId); + } else { + log.warn("电单车自动建档-modelId未配置或无效, pileSn:{}, configuredModelId:{}, source:{}", + pileSn, ebikeAutoRegisterModelId, source); + } + basicInfo.setProductionDate(new Date()); + basicInfo.setCreateBy(Constants.SYSTEM); + basicInfo.setDelFlag(DelFlagEnum.NORMAL.getValue()); + basicInfo.setRemark("自动建档:" + source + "; portNumber=" + portNumber); + return basicInfo; + } + + private List buildMissingEBikeConnectorInfoList(String pileSn, int portNumber, List existingConnectorInfoList) { + Set existingConnectorCodeSet = CollectionUtils.isEmpty(existingConnectorInfoList) + ? Collections.emptySet() + : existingConnectorInfoList.stream() + .map(PileConnectorInfo::getPileConnectorCode) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + + List connectorInfoList = Lists.newArrayList(); + for (int i = 1; i < portNumber + 1; i++) { + String connectorCode = String.format("%1$02d", i); + String pileConnectorCode = pileSn + connectorCode; + if (existingConnectorCodeSet.contains(pileConnectorCode)) { + continue; + } + PileConnectorInfo connectorInfo = new PileConnectorInfo(); + connectorInfo.setPileSn(pileSn); + connectorInfo.setPileConnectorCode(pileConnectorCode); + connectorInfo.setStatus(Constants.ZERO); + connectorInfo.setCreateBy(Constants.SYSTEM); + connectorInfo.setDelFlag(DelFlagEnum.NORMAL.getValue()); + connectorInfoList.add(connectorInfo); + } + return connectorInfoList; + } + + private void cleanEBikeAutoRegisterCache(String pileSn, Long stationId) { + List keys = Lists.newArrayList(); + keys.add(CacheConstants.SELECT_PILE_BASIC_INFO_BY_SN + pileSn); + keys.add(CacheConstants.SELECT_PILE_CONNECTOR_INFO_LIST + pileSn); + keys.add(CacheConstants.PILE_DETAIL_KEY + pileSn); + if (stationId != null) { + keys.add(CacheConstants.GET_PILE_LIST_BY_STATION_ID + stationId); + } + redisCache.deleteObject(keys); + } @Override public java.math.BigDecimal getMonthlyAvgElectricity(String monthStartDate, String monthEndDate) { diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java index 4c37c51a8..866587397 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java @@ -777,6 +777,10 @@ public class PileConnectorInfoServiceImpl implements PileConnectorInfoService { String pileSn = pileConnectorCode.substring(0, pileConnectorCode.length() - 2); // 只修改一个枪口的状态 num = pileConnectorInfoMapper.updateConnectorStatus(pileConnectorCode, status); + if (num <= 0) { + log.warn("更新枪口状态-更新结果为0, pileConnectorCode:{}, status:{}, 状态描述:{}", + pileConnectorCode, status, PileConnectorDataBaseStatusEnum.getStatusDescription(status)); + } deleteRedisByPileSnOrPileConnectorCode(pileSn, pileConnectorCode); redisCache.setCacheObject(redisKey, status, CacheConstants.cache_expire_time_6m);