12 Commits

27 changed files with 828 additions and 296 deletions

View File

@@ -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 = 自动建档:<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`
- 不建议收到端口数变小后自动删除端口。
- 不建议没有开关、没有日志、没有锁就直接让心跳自动插库。

View File

@@ -76,7 +76,7 @@ public class JumpController extends BaseController {
try {
// 进入充电桩详情做一下鉴权
String memberId = getMemberIdByAuthorization(request);
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn);
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn, memberId);
addMember2MemberGroup(memberId, vo);
response = new RestApiResponse<>(vo);
} catch (BusinessException e) {
@@ -114,7 +114,7 @@ public class JumpController extends BaseController {
try {
// 进入充电桩详情做一下鉴权
String memberId = getMemberIdByAuthorization(request);
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn);
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn, memberId);
addMember2MemberGroup(memberId, vo);
response = new RestApiResponse<>(vo);
} catch (BusinessException e) {
@@ -176,7 +176,7 @@ public class JumpController extends BaseController {
memberId = getMemberIdByAuthorization(request);
}
AppletPileDetailVO vo = pileService.getConnectorDetail(pileConnectorCode);
AppletPileDetailVO vo = pileService.getConnectorDetail(pileConnectorCode, memberId);
logger.info("查询充电枪口详情, pileConnectorCode:{}, vo:{}", pileConnectorCode, JSON.toJSONString(vo));
if (StringUtils.isNotBlank(memberId)) {
addMember2MemberGroup(memberId, vo);

View File

@@ -42,7 +42,7 @@ public class JumpXixiaoController extends BaseController {
logger.info("User-Agent:{}", request.getHeader("user-agent"));
RestApiResponse<?> response = null;
try {
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn);
AppletPileDetailVO vo = pileService.getPileDetailByPileSn(pileSn, null);
response = new RestApiResponse<>(vo);
} catch (BusinessException e) {
logger.warn("app-xcx-h5查询充电桩详情 warn", e);
@@ -65,7 +65,7 @@ public class JumpXixiaoController extends BaseController {
logger.info("User-Agent:{}", request.getHeader("user-agent"));
RestApiResponse<?> response = null;
try {
AppletPileDetailVO vo = pileService.getConnectorDetail(pileConnectorCode);
AppletPileDetailVO vo = pileService.getConnectorDetail(pileConnectorCode, null);
response = new RestApiResponse<>(vo);
} catch (BusinessException e) {
logger.warn("app-xcx-h5查询充电枪口详情 warn param:{}", pileConnectorCode, e);

View File

@@ -693,6 +693,9 @@ public class MemberService {
if (StringUtils.isNotBlank(dto.getNickName())) {
memberBasicInfo.setNickName(dto.getNickName());
}
if (StringUtils.isNotBlank(dto.getDefaultChargeAmount())) {
memberBasicInfo.setDefaultChargeAmount(new BigDecimal(dto.getDefaultChargeAmount()));
}
memberBasicInfoService.updateMemberBasicInfo(memberBasicInfo);
}

View File

@@ -220,7 +220,7 @@ public class PileService {
return vo;
}
public AppletPileDetailVO getPileDetailByPileSn(String param) throws Exception {
public AppletPileDetailVO getPileDetailByPileSn(String param, String memberId) throws Exception {
AppletPileDetailVO vo = null;
log.info("查询充电枪口详情-getPileDetailByPileSn, param:{}", param);
if (StringUtils.isBlank(param)) {
@@ -253,6 +253,22 @@ public class PileService {
throw new BusinessException(ReturnCodeEnum.CODE_STATION_IS_NOT_OPEN);
}
// 判断该桩是否为个人桩
if (StringUtils.isNotBlank(memberId)) {
List<MemberVO> memberVOS = pileMemberRelationService.selectMemberList(pileSn);
if (CollectionUtils.isNotEmpty(memberVOS)) {
// 说明为个人桩, 判断会员信息
// 将memberId收集成list
List<String> memberIdList = memberVOS.stream()
.map(MemberVO::getMemberId)
.collect(Collectors.toList());
// 如果会员没有绑定该桩,说明没有权限进行操作
if (!memberIdList.contains(memberId)) {
throw new BusinessException(ReturnCodeEnum.CODE_MEMBER_NOT_HAVE_PILE_PERMISSION);
}
}
}
// 查询充电桩下枪口信息
CompletableFuture<List<ConnectorInfoVO>> connectorInfoListFuture = CompletableFuture.supplyAsync(() -> pileConnectorInfoService.selectConnectorInfoList(pileSn), executor);
// log.info("查询充电枪口详情-supplyAsync-selectConnectorInfoList:{}", connectorInfoListFuture);
@@ -299,7 +315,7 @@ public class PileService {
* @throws ExecutionException
* @throws InterruptedException
*/
public AppletPileDetailVO getConnectorDetail(String pileConnectorCode) throws Exception {
public AppletPileDetailVO getConnectorDetail(String pileConnectorCode, String memberId) throws Exception {
log.info("查询充电枪口详情, pileConnectorCode:{}", pileConnectorCode);
PileConnectorDetailVO pileConnectorDetailVO = queryPileConnectorDetail(pileConnectorCode);
log.info("查询充电枪口详情, pileConnectorDetailVO:{}", JSON.toJSONString(pileConnectorDetailVO));
@@ -314,7 +330,7 @@ public class PileService {
}
}
String pileSn = pileConnectorDetailVO.getPileSn();
AppletPileDetailVO resultVO = getPileDetailByPileSn(pileSn);
AppletPileDetailVO resultVO = getPileDetailByPileSn(pileSn, memberId);
log.info("查询充电枪口详情getConnectorDetail, pileSn:{}, pileConnectorDetailVO:{}, resultVO:{}", pileSn, JSON.toJSONString(pileConnectorDetailVO), JSON.toJSONString(resultVO));
List<ConnectorInfoVO> connectorInfoList = resultVO.getConnectorInfoList();
if (connectorInfoList.size() > 1 && !StringUtils.equals(pileConnectorDetailVO.getChargePortType(), "3")) {
@@ -332,7 +348,7 @@ public class PileService {
return resultVO;
}
public AppletPileDetailVO getConnectorDetailV2(String pileConnectorCode) throws Exception {
public AppletPileDetailVO getConnectorDetailV2(String pileConnectorCode, String memberId) throws Exception {
log.info("查询充电枪口详情, pileConnectorCode:{}", pileConnectorCode);
PileConnectorDetailVO pileConnectorDetailVO = queryPileConnectorDetail(pileConnectorCode);
log.info("查询充电枪口详情, pileConnectorDetailVO:{}", JSON.toJSONString(pileConnectorDetailVO));
@@ -347,7 +363,7 @@ public class PileService {
}
}
String pileSn = pileConnectorDetailVO.getPileSn();
AppletPileDetailVO resultVO = getPileDetailByPileSn(pileSn);
AppletPileDetailVO resultVO = getPileDetailByPileSn(pileSn, memberId);
log.info("查询充电枪口详情getConnectorDetail, pileSn:{}, pileConnectorDetailVO:{}, resultVO:{}", pileSn, JSON.toJSONString(pileConnectorDetailVO), JSON.toJSONString(resultVO));
List<ConnectorInfoVO> connectorInfoList = resultVO.getConnectorInfoList();
if (connectorInfoList.size() > 1 && !StringUtils.equals(pileConnectorDetailVO.getChargePortType(), "3")) {

View File

@@ -103,6 +103,17 @@ pagehelper:
supportMethodsArguments: true
params: count=countSql
# 电单车自动建档配置
ebike:
auto-register:
# 设备未上报注册包时允许通过0x21心跳兜底创建桩和端口基础数据
enabled: true
# 默认归属信息建议配置为“待配置设备”运营商/站点,后续由后台人工调整
merchant-id: 1
station-id: 2
# 必须按环境配置为有效的电单车型号ID0表示未配置仍会创建基础数据但详情查询可能受影响
model-id: 0
# 防止XSS攻击
xss:
# 过滤开关

View File

@@ -1,188 +1 @@
002212025121618122710846946041776041984
002212025123013543010851954554563244032
002212026010104322810852537890272755712
002212025112522073210839395053787557888
002212025120720413410843722075120734208
002212026010918332710855648635420176384
002212026012207545810860198996873125888
002212026010910374210855528908832141312
002212025111709485410836310070675070976
002212025112816413110840400174906601472
002212026013017454210863246761760948224
002212025113022210510841210404460548096
002212025111316581810834968580149411840
002212026012211165810860249831925686272
002212026010200085610852833959422513152
002212026012118555110860002926394146816
002212025121209334710845365961304027136
002212026012017103710859614052845150208
002212025120319535010842260512643993600
002212025121504451010846380494055161856
002212025120119313110841530122060828672
002212025122523192310850284773435035648
002212026013017394110863245248292532224
002212026012705333310861975346717986816
002212026012009102510859493208189870080
002212026012819171110862545009394896896
002212026020113151710863903484339363840
002212026010614041510854493722241695744
002212025122717154010850918016471597056
002212026010415405910853793291316260865
002212025120313035610842157357671374848
002212025120405501810842410617678213120
002212025100710292810821462377338449920
002212025090513433510809914813501046784
002212025121308145310845708494798307328
002212025102800250710828920431341121536
002212026011209350810856600323798896640
002212025093019381810819063779938525184
002212026012914130110862830851761950720
002212025102723251810828905377354887168
002212026011416122610857425084242272256
002212025103123200410830353610908577792
002212025092723331010818035721160736768
002212025112019113210837538826677252096
002212026010819084710855295138321465344
002212025102319241310827395154634309632
002212025102718533110828836980470480896
002212025121811351210847570843389894656
002212025112811395410840324268695461888
002212026011315270810857051297583755264
002212026011508300810857671132671950848
002212025121818501810847680341601288192
002212025122012235910848307895332904960
002212025111214424910834572096455241728
002212025101503332310824256769765740544
002212025101014122910822605665181626368
002212025123111295910852280573656604672
002212025122912321410851571464460337152
002212025122419071810849858946427277312
002212025121507533010846427888914513920
002212026011820155110858935895485870080
002212025121716382410847284757996490752
002212025102410154910827619533163728896
002212025111019480710833924154224484352
002212026012519531010861466899182305280
002212025121912515310847952530791710720
002212025091911133110814950481046421504
002212026010522171010854255381420228608
002212025121013552310844707021574512640
002212025091415572210813209974666567680
002212025111721214310836484421810593792
002212025090119241010808550972619939840
002212026012607395310861644753064984576
002212025090412412210809536768898379776
002212026011921282510859316542855516160
002212025092118550910815791429036949504
002212025121713242610847235944673849344
002212025092920124510818710059610238976
002212025101309242410823620330208473088
002212025090618454910810353263698137088
002212025111818080810836798092730814464
002212025110621300210832500248041185280
002212025122311324710849382174300438528
002212026012722323510862231793603231744
002212026010220060610853135235943292928
002212025111623214410836152237925052416
002212025110913170210833463344691904512
002212025112217360210838239567226093568
002212025111920191110837193462170267648
002212025102816240010829161740781883393
002212025101313554710823688625817227264
002212025092518534110817240611673935872
002212025120423191310842674587056205824
002212025102516531010828081920610299904
002212025101720434410825240839836921856
002212025101918085910825926669907476480
002212026012419092410861093500920561664
002212025091219531510812544559669997568
002212025123008312910851873263692075008
002212025100221475910819821189846122496
002212025112511232110839232941986975744
002212025121919483610848057399154188288
002212025092214454610816091056525180928
002212026020119090710863992529354534912
002212025111515305510835671363391528960
002212025112317000010838592887014834176
002212026010508493510854052147955040256
002212026010518415810854201223866535936
002212026011522191510857879786327154688
002212025091513024210813528407530708992
002212025103018083310829912829399547904
002212026011513542310857752731270836224
002212025100414103310820430849850728448
002212026012714242210862108933408727040
002212025121418303510846225829766238208
002212025100422201910820554104661979136
002212025122216352810849095961576468480
002212025091216301410812493470157410304
002212025110309523010831237547117752320
002212026011414155910857395779063824384
002212025110812523510833094804327608320
002212025092322552810816576681653223424
002212026010117005410852726238882861056
002212025091007290010811632489596358656
002212025100217134710819752185085255680
002212025101118563210823039533252460544
002212026010409041510853693449663447040
002212025100914052510822241499388157952
002212025100916430710822281184114229248
002212025122514361110850153105390280704
002212025101818103810825564697454292992
002212026010711420210854820320350359552
002212025111714341610836381885107752960
002212025110518584710832099797764382720
002212026010311552410853374135496413184
002212025122608430110850426617083006976
002212025122816133210851264768693276672
002212026020314034410864640454610989056
002212025120219364610841893827348922368
002212025120822291510844111562225205248
002212025100711442310821481231221641216
002212025100815190010821897628623638528
002212025092910324910818564115468087296
002212025121418490310846230476820201472
002212026010523424310854276910094815232
002212025100422130310820552276624748544
002212025112109154310837751268539756544
002212026011813062010858827804463374336
002212025091019372610811815805750501376
002212025121311371710845759431650816000
002212025092221314710816193237265862656
002212025122408410510849701355378061312
002212025112221494110838303398304419840
002212026011214551110856680867706507264
002212025123107215410852218142670508032
002212025083119251510808188859560914944
002212025092308403010816361524222107648
002212025121118084510845133170435940352
002212025091209403510812390376895553536
002212025112518515910839345842035605504
002212025101212002610823297209891024896
002212025112618380410839704729997799424
002212025091813145810814618655161491456
002212026011709375210858412953370951680
002212025112704025110839846863925022720
002212026011113272010856296374202064896
002212025102923231610829629641771081728
002212025110408044010831572796683010048
002212026012218400210860361332279767040
002212026010707244110854755555972276224
002212025092900192010818409727185489920
002212026012619023510861816557700186112
002212025102302461410827144007524765696
002212026012705214410861972373845626880
002212025121113521510845068621057875968
002212025112408354410838828369879498752
002212025123120392310852418836375592960
002212025122219151210849136159274299392
002212025092507595610817076088643436544
002212025122413121010849769573971103744
002212025121319505510845883657896472576
002212025120709385910843555330682667009
002212025122113262310848685986643464192
002212026011616174010858151177270140928
002212025091618584210813980384911839232
002212025102011233010826187014639308800
002212025121601225210846691971442135040
002212025090523352110810063740095320064
002212026052916015810906344811847229440

View File

@@ -199,6 +199,8 @@ public enum ReturnCodeEnum {
CODE_ALREADY_AN_USER("00400024", "您已经是此充电桩用户, 无需再次绑定"),
CODE_MEMBER_NOT_HAVE_PILE_PERMISSION("00700004", "您没有此桩的权限!"),
/* 个人桩 end */
CODE_THIS_CARNO_HAS_BEEN_BINDING("00500001", "当前车牌号已经绑定,请检查!"),
@@ -231,8 +233,7 @@ public enum ReturnCodeEnum {
CODE_THIS_VIN_HAS_BEEN_BINDING("00700002", "该vin已被绑定请检查!"),
CODE_THIS_VIN_INFO_IS_NULL("007000003", "未查到该vin信息"),
;
CODE_THIS_VIN_INFO_IS_NULL("007000003", "未查到该vin信息");
private String value;

View File

@@ -118,21 +118,23 @@ public class YouDianProtocolDecoder extends ByteToMessageDecoder {
ByteBuf frame = null;
try {
// 检查剩余数据是否足够
if (buffer.readableBytes() < HEADER_LENGTH_DNY + 1) {
if (buffer.readableBytes() < HEADER_LENGTH_DNY + 2) {
buffer.readerIndex(beginReader);
return;
}
// 获取消息长度
int length = buffer.getUnsignedByte(beginReader + HEADER_LENGTH_DNY);
// DNY协议长度域为2字节小端模式。长度不包含包头和长度域本身。
int length = buffer.getUnsignedByte(beginReader + HEADER_LENGTH_DNY)
| (buffer.getUnsignedByte(beginReader + HEADER_LENGTH_DNY + 1) << 8);
// log.info("获取消息长度, length:{}", length);
int frameLength = HEADER_LENGTH_DNY + 2 + length;
// 检查剩余数据是否足够
if (buffer.readableBytes() < HEADER_LENGTH_DNY + 1 + length) {
if (buffer.readableBytes() < frameLength) {
buffer.readerIndex(beginReader);
return;
}
// 读取 data 数据
frame = buffer.retainedSlice(beginReader, HEADER_LENGTH_DNY + length + 2);
buffer.readerIndex(beginReader + HEADER_LENGTH_DNY + length + 2);
frame = buffer.retainedSlice(beginReader, frameLength);
buffer.readerIndex(beginReader + frameLength);
out.add(frame);
} finally {
// if (frame != null) {

View File

@@ -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);

View File

@@ -1,8 +1,10 @@
package com.jsowell.netty.handler.electricbicycles;
import com.alibaba.fastjson2.JSON;
import com.jsowell.common.YouDianUtils;
import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ebike.EBikeDataProtocol;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.netty.factory.EBikeOperateFactory;
import com.jsowell.pile.domain.ebike.EBikeCommandEnum;
@@ -39,12 +41,46 @@ public class RegistrationHandler extends AbstractEBikeHandler {
*/
@Override
public byte[] supplyProcess(EBikeDataProtocol dataProtocol, ChannelHandlerContext ctx) {
EBikeMessageCmd20 message;
try {
// 解析字节数组
EBikeMessageCmd20 message = new EBikeMessageCmd20(dataProtocol.getBytes());
message = new EBikeMessageCmd20(dataProtocol.getBytes());
} catch (Exception e) {
handleRegistrationParseError(dataProtocol, ctx, e);
return getResult(dataProtocol, Constants.zeroByteArray);
}
// 保存时间
saveLastTimeAndCheckChannel(message.getPhysicalId() + "", ctx);
log.info("设备注册包:{}", JSON.toJSONString(message));
try {
pileBasicInfoService.registrationEBikePile(message);
} catch (Exception e) {
log.error("设备注册包自动建档失败, pileSn:{}, portNumber:{}, msg:{}",
message.getPhysicalId(), message.getPortNumber(), BytesUtil.binary(dataProtocol.getBytes(), 16), e);
}
return getResult(dataProtocol, Constants.zeroByteArray);
}
private void handleRegistrationParseError(EBikeDataProtocol dataProtocol, ChannelHandlerContext ctx, Exception e) {
String pileSn = null;
int portNumber = 0;
try {
pileSn = YouDianUtils.convertToPhysicalId(dataProtocol.getPhysicalId()) + "";
byte[] msgBody = dataProtocol.getMsgBody();
if (msgBody != null && msgBody.length >= 3) {
portNumber = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(msgBody, 2, 1));
}
saveLastTimeAndCheckChannel(pileSn, ctx);
log.error("设备注册包解析失败, pileSn:{}, portNumber:{}, msg:{}, msgBodyLength:{}",
pileSn, portNumber, BytesUtil.binary(dataProtocol.getBytes(), 16),
msgBody == null ? 0 : msgBody.length, e);
if (portNumber > 0) {
pileBasicInfoService.ensureEBikePileRegistered(pileSn, portNumber, "registration_0x20_parse_fallback");
}
} catch (Exception fallbackException) {
log.error("设备注册包解析失败后兜底处理失败, pileSn:{}, portNumber:{}, msg:{}",
pileSn, portNumber, BytesUtil.binary(dataProtocol.getBytes(), 16), fallbackException);
}
}
}

View File

@@ -346,7 +346,11 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
}
dto.setConnectorCode(connectorCode);
dto.setTransactionCode(transactionCode);
if (memberWalletVO.getDefaultChargeAmount() != null && memberWalletVO.getDefaultChargeAmount().compareTo(BigDecimal.ZERO) > 0) {
dto.setChargeAmount(memberWalletVO.getDefaultChargeAmount());
}else {
dto.setChargeAmount(new BigDecimal(accountBalance));
}
dto.setPayMode(payMode);
dto.setStartMode(StringUtils.equals("01", startMode) ? "2" : "5");
dto.setMemberId(memberWalletVO.getMemberId());

View File

@@ -94,6 +94,12 @@ public class MemberBasicInfo extends BaseEntity {
@Excel(name = "所属运营商")
private Long merchantId;
/**
* 默认充电金额
*/
@Excel(name = "默认充电金额")
private BigDecimal defaultChargeAmount;
/**
* 删除标识0-正常1-删除)
*/
@@ -111,6 +117,7 @@ public class MemberBasicInfo extends BaseEntity {
.append("avatarUrl", avatarUrl)
.append("mobileNumber", mobileNumber)
.append("merchantId", merchantId)
.append("defaultChargeAmount", defaultChargeAmount)
.append("delFlag", delFlag)
.toString();
}

View File

@@ -14,6 +14,8 @@ import java.util.Arrays;
@Builder
@ToString
public class AbsEBikeMessage2 {
protected static final int DATA_START_INDEX = 12;
protected String header; // 包头 (3字节)
protected int msgLength; // 长度 (2字节)
protected int physicalId; // 物理ID (4字节)
@@ -59,7 +61,7 @@ public class AbsEBikeMessage2 {
startIndex += length;
length = 1;
byte[] commandBytes = BytesUtil.copyBytes(messageBytes, startIndex, length);
this.command = BytesUtil.bcd2StrLittle(commandBytes);
this.command = BytesUtil.printHexBinary(commandBytes);
// 读取数据, 暂不处理, 交给子类处理
// byte[] dataBytes = BytesUtil.copyBytes(messageBytes, startIndex, length);
@@ -72,4 +74,26 @@ public class AbsEBikeMessage2 {
public byte[] getMessageBytes() {
return null;
};
protected int getBodyLength() {
return this.msgLength - 9;
}
protected int getDataEndIndex(byte[] messageBytes) {
return messageBytes.length - 2;
}
protected boolean hasDataBytes(byte[] messageBytes, int startIndex, int length) {
return length >= 0
&& startIndex >= DATA_START_INDEX
&& startIndex + length <= getDataEndIndex(messageBytes);
}
protected String getExtendedDataHex(byte[] messageBytes, int startIndex) {
int extendedLength = getDataEndIndex(messageBytes) - startIndex;
if (extendedLength <= 0) {
return null;
}
return BytesUtil.printHexBinary(BytesUtil.copyBytes(messageBytes, startIndex, extendedLength));
}
}

View File

@@ -36,7 +36,7 @@ public class EBikeMessageCmd02 extends AbsEBikeMessage2 {
/**
* 时间戳
*/
// private String timestamp;
private String timestamp;
/**
* 卡号2字节数
@@ -51,7 +51,7 @@ public class EBikeMessageCmd02 extends AbsEBikeMessage2 {
public EBikeMessageCmd02(byte[] messageBytes) {
super(messageBytes);
int startIndex = 12;
int startIndex = DATA_START_INDEX;
int length = 4;
this.cardId = BytesUtil.bcd2Str(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
@@ -67,15 +67,20 @@ public class EBikeMessageCmd02 extends AbsEBikeMessage2 {
length = 2;
this.cardBalance = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
// length = 4;
// this.timestamp = BytesUtil.bytesToIntLittle(Arrays.copyOfRange(dataBytes, startIndex, startIndex = startIndex + length)) + "";
if (messageBytes.length > startIndex) {
startIndex += length;
length = 1;
card2Length = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
length = 4;
if (hasDataBytes(messageBytes, startIndex, length)) {
this.timestamp = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
startIndex += length;
}
this.card2Code = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
length = 1;
if (hasDataBytes(messageBytes, startIndex, length)) {
card2Length = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
startIndex += length;
}
if (card2Length > 0 && hasDataBytes(messageBytes, startIndex, card2Length)) {
this.card2Code = BytesUtil.printHexBinary(BytesUtil.copyBytes(messageBytes, startIndex, card2Length));
}
}
}

View File

@@ -99,7 +99,7 @@ public class EBikeMessageCmd06 extends AbsEBikeMessage2 {
/**
* 上发指令当时的时间,有时候不准确,该字段属于调试使用,服务器无需关心此字段
*/
// private String timestamp;
private String timestamp;
/**
* 占位时长:(充电柜专用,其他设备忽略此字段)表示充满后占用设备的时长,单位为分钟
@@ -108,8 +108,9 @@ public class EBikeMessageCmd06 extends AbsEBikeMessage2 {
public EBikeMessageCmd06(byte[] messageBytes) {
super(messageBytes);
int dataEndIndex = getDataEndIndex(messageBytes);
int startIndex = 12;
int startIndex = DATA_START_INDEX;
int length = 1;
this.connectorCode = YouDianUtils.convertPortNumberToString(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)));
@@ -160,31 +161,63 @@ public class EBikeMessageCmd06 extends AbsEBikeMessage2 {
startIndex += length;
length = 2;
if (hasPeakPower(dataEndIndex - startIndex) && hasBytes(messageBytes, startIndex, length)) {
this.peakPower = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(new BigDecimal("0.1")).toString();
startIndex += length;
}
length = 2;
if (hasBytes(messageBytes, startIndex, length)) {
this.voltage = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(new BigDecimal("0.1")).toString();
startIndex += length;
}
length = 2;
if (hasBytes(messageBytes, startIndex, length)) {
this.current = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(new BigDecimal("0.001")).toString();
startIndex += length;
}
length = 1;
this.ambientTemperature = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.subtract(new BigDecimal("65")).toString();
if (hasBytes(messageBytes, startIndex, length)) {
this.ambientTemperature = parseTemperature(messageBytes, startIndex, length);
startIndex += length;
}
length = 1;
this.portTemperature = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.subtract(new BigDecimal("65")).toString();
if (hasBytes(messageBytes, startIndex, length)) {
this.portTemperature = parseTemperature(messageBytes, startIndex, length);
startIndex += length;
}
length = 4;
if (hasBytes(messageBytes, startIndex, length)) {
this.timestamp = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
startIndex += length;
}
length = 2;
if (hasBytes(messageBytes, startIndex, length)) {
this.occupancyTime = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
}
}
private boolean hasBytes(byte[] messageBytes, int startIndex, int length) {
return hasDataBytes(messageBytes, startIndex, length);
}
private boolean hasPeakPower(int optionalLength) {
return optionalLength == 2 || optionalLength == 8 || optionalLength >= 12;
}
private String parseTemperature(byte[] messageBytes, int startIndex, int length) {
int value = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
if (value == 0) {
return "0";
}
return String.valueOf(value - 65);
}
}

View File

@@ -12,6 +12,8 @@ import java.math.BigDecimal;
@Setter
@ToString(callSuper = true)
public class EBikeMessageCmd20 extends AbsEBikeMessage2 {
private static final BigDecimal VERSION_FACTOR = new BigDecimal("0.01");
/**
* 固件版本如100则表示V1.00版本
*/
@@ -42,13 +44,30 @@ public class EBikeMessageCmd20 extends AbsEBikeMessage2 {
*/
private String powerBoardVersion;
/**
* 设备分时计费功能0=不支持1=支持。旧协议无此字段。
*/
private Integer deviceSeparateBillingSupport;
/**
* TC模式。旧协议无此字段。
*/
private Integer tcMode;
/**
* 扩展数据:标准注册字段后的原始扩展字段
*/
private String extendedData;
public EBikeMessageCmd20(byte[] messageBytes) {
super(messageBytes);
if (getBodyLength() < 8) {
throw new IllegalArgumentException("Invalid command 0x20 body length: " + getBodyLength());
}
int startIndex = 12;
int startIndex = DATA_START_INDEX;
int length = 2;
this.firmwareVersion = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(new BigDecimal("0.1")).toString();
this.firmwareVersion = parseVersion(messageBytes, startIndex, length);
startIndex += length;
length = 1;
@@ -64,10 +83,34 @@ public class EBikeMessageCmd20 extends AbsEBikeMessage2 {
startIndex += length;
length = 1;
this.workMode = BytesUtil.bcd2StrLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
this.workMode = BytesUtil.printHexBinary(BytesUtil.copyBytes(messageBytes, startIndex, length));
startIndex += length;
length = 2;
this.powerBoardVersion = BytesUtil.bcd2StrLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
this.powerBoardVersion = parseVersion(messageBytes, startIndex, length);
startIndex += length;
int extendedStartIndex = startIndex;
length = 1;
if (hasBytes(messageBytes, startIndex, length)) {
this.deviceSeparateBillingSupport = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
startIndex += length;
}
length = 1;
if (hasBytes(messageBytes, startIndex, length)) {
this.tcMode = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
}
this.extendedData = getExtendedDataHex(messageBytes, extendedStartIndex);
}
private String parseVersion(byte[] messageBytes, int startIndex, int length) {
return new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(VERSION_FACTOR).toString();
}
private boolean hasBytes(byte[] messageBytes, int startIndex, int length) {
return hasDataBytes(messageBytes, startIndex, length);
}
}

View File

@@ -53,7 +53,7 @@ public class EBikeMessageCmd21 extends AbsEBikeMessage2 {
super(messageBytes);
// 读取结果
int startIndex = 12;
int startIndex = DATA_START_INDEX;
int length = 2;
this.voltage = new BigDecimal(BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)))
.multiply(new BigDecimal("0.1")).toString();
@@ -78,14 +78,18 @@ public class EBikeMessageCmd21 extends AbsEBikeMessage2 {
startIndex += length;
length = 1;
if (hasDataBytes(messageBytes, startIndex, length)) {
this.rssi = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length)) + "";
}
startIndex += length;
length = 1;
if (hasDataBytes(messageBytes, startIndex, length)) {
int i = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(messageBytes, startIndex, length));
if (i > 65) {
if (i > 0) {
i = i - 65;
}
this.temperature = i + "";
}
}
}

View File

@@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 用户注册前台参数
*
@@ -35,4 +37,9 @@ public class MemberRegisterDTO {
* 手机号码
*/
private String mobileNumber;
/**
* 默认充电启动金额
*/
private String defaultChargeAmount;
}

View File

@@ -250,6 +250,14 @@ public interface PileBasicInfoService {
*/
void registrationEBikePile(EBikeMessageCmd20 message);
/**
* 确保电单车桩基础数据已注册
* @param pileSn 桩号
* @param portNumber 端口数量
* @param source 触发来源
*/
void ensureEBikePileRegistered(String pileSn, int portNumber, String source);
List<PileDetailInfoVO> getPileDetailInfoList(String stationId);
/**

View File

@@ -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,52 +1609,143 @@ 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<PileBasicInfo> basicInfoList = Lists.newArrayList();
List<PileConnectorInfo> 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;
}
// 批量入库
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<PileConnectorInfo> connectorInfoList = pileConnectorInfoService.selectPileConnectorInfoList(pileSn);
List<PileConnectorInfo> missingConnectorInfoList = buildMissingEBikeConnectorInfoList(pileSn, portNumber, connectorInfoList);
if (pileBasicInfo == null) {
PileBasicInfo basicInfo = buildAutoRegisterEBikeBasicInfo(pileSn, portNumber, source);
PileTransactionDTO transactionDTO = PileTransactionDTO.builder()
.pileBasicInfoList(basicInfoList)
.pileConnectorInfoList(connectorInfoList)
.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<PileConnectorInfo> buildMissingEBikeConnectorInfoList(String pileSn, int portNumber, List<PileConnectorInfo> existingConnectorInfoList) {
Set<String> existingConnectorCodeSet = CollectionUtils.isEmpty(existingConnectorInfoList)
? Collections.emptySet()
: existingConnectorInfoList.stream()
.map(PileConnectorInfo::getPileConnectorCode)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
List<PileConnectorInfo> 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<String> 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

View File

@@ -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);

View File

@@ -22,6 +22,11 @@ public class ConfirmStartChargingMemberVO {
*/
private String plateNumber;
/**
* 默认充电金额
*/
private BigDecimal defaultChargeAmount;
/**
* 本金金额
*/

View File

@@ -44,6 +44,11 @@ public class MemberVO {
*/
private String merchantId;
/**
* 默认充电金额
*/
private BigDecimal defaultChargeAmount;
/**
* 运营商名称
*/

View File

@@ -17,6 +17,7 @@
<result property="avatarUrl" column="avatar_url" />
<result property="mobileNumber" column="mobile_number" />
<result property="merchantId" column="merchant_id" />
<result property="defaultChargeAmount" column="default_charge_amount"/>
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="createBy" column="create_by" />
@@ -34,7 +35,7 @@
<sql id="Base_Column_List">
<!--@mbg.generated PileBillingTemplate-->
id, member_id, open_id, buyer_id, license_plate_number, nick_name,logic_card, physics_card, status, avatar_url,
mobile_number, merchant_id, remark,
mobile_number, merchant_id, default_charge_amount, remark,
create_time, create_by, update_time, update_by, del_flag
</sql>
@@ -70,6 +71,7 @@
<if test="avatarUrl != null">avatar_url,</if>
<if test="mobileNumber != null">mobile_number,</if>
<if test="merchantId != null">merchant_id,</if>
<if test="defaultChargeAmount != null">default_charge_amount,</if>
<if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if>
<if test="createBy != null">create_by,</if>
@@ -88,6 +90,7 @@
<if test="avatarUrl != null">#{avatarUrl},</if>
<if test="mobileNumber != null">#{mobileNumber},</if>
<if test="merchantId != null">#{merchantId},</if>
<if test="defaultChargeAmount != null">#{defaultChargeAmount},</if>
<if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="createBy != null">#{createBy},</if>
@@ -110,6 +113,7 @@
<if test="avatarUrl != null">avatar_url = #{avatarUrl},</if>
<if test="mobileNumber != null">mobile_number = #{mobileNumber},</if>
<if test="merchantId != null">merchant_id = #{merchantId},</if>
<if test="defaultChargeAmount != null">default_charge_amount = #{defaultChargeAmount},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="createBy != null">create_by = #{createBy},</if>
@@ -201,6 +205,7 @@
t1.avatar_url as avatarUrl,
t1.mobile_number as mobileNumber,
t1.merchant_id as merchantId,
t1.default_charge_amount as defaultChargeAmount,
t1.create_time as createTime
<!--t2.principal_balance as principalBalance,
t2.gift_balance as giftBalance-->
@@ -327,6 +332,7 @@
<select id="queryMemberInfoByVinCode" resultType="com.jsowell.pile.vo.base.ConfirmStartChargingMemberVO">
select
t1.member_id as memberId,
t1.default_charge_amount as defaultChargeAmount,
t2.license_plate_number as plateNumber,
t3.principal_balance as principalBalance,
t3.gift_balance as giftBalance