mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-05-15 15:28:41 +08:00
同步充电桩数据
This commit is contained in:
322
doc/JCPP_JSON消息分区消费方案.md
Normal file
322
doc/JCPP_JSON消息分区消费方案.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# JCPP JSON 消息分区消费方案
|
||||
|
||||
## 概述
|
||||
|
||||
本方案实现了基于 JSON 格式的 JCPP 消息分区消费,确保同一充电桩(相同 pileCode)的消息按顺序处理,避免并发导致的消息乱序问题。
|
||||
|
||||
## 核心特性
|
||||
|
||||
1. **消息顺序保证**:同一充电桩的消息按顺序处理
|
||||
2. **分区消费**:10 个分区,基于 pileCode 的 Hash 分区
|
||||
3. **单线程消费**:每个分区单线程消费,保证顺序
|
||||
4. **JSON 格式**:保持原有 JSON 格式,无需 Protobuf
|
||||
5. **统一处理**:整合所有消息类型的处理逻辑
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 分区策略
|
||||
|
||||
- **分区数量**:默认 10 个分区(可配置)
|
||||
- **分区算法**:MurmurHash3_128(与 JCPP 保持一致)
|
||||
- **分区键**:pileCode(充电桩编码)
|
||||
- **消费模式**:每个分区单线程消费,保证顺序
|
||||
|
||||
### 2. 核心组件
|
||||
|
||||
```
|
||||
jsowell-pile/src/main/java/com/jsowell/pile/jcpp/
|
||||
├── config/
|
||||
│ └── JcppPartitionQueueConfig.java # 分区队列配置
|
||||
├── consumer/
|
||||
│ └── JcppJsonPartitionConsumer.java # JSON 消息消费者
|
||||
├── service/
|
||||
│ ├── IJcppJsonMessageHandler.java # 消息处理器接口
|
||||
│ └── impl/
|
||||
│ └── JcppJsonMessageHandlerImpl.java # 消息处理器实现
|
||||
└── util/
|
||||
└── JcppPartitionCalculator.java # 分区计算器
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 配置文件
|
||||
|
||||
在 `application-{env}.yml` 中添加:
|
||||
|
||||
```yaml
|
||||
# JCPP 配置
|
||||
jcpp:
|
||||
rabbitmq:
|
||||
# 分区数量(与 JCPP 保持一致)
|
||||
partition-count: 10
|
||||
# Exchange 名称
|
||||
exchange: jcpp.uplink.exchange
|
||||
# 队列前缀
|
||||
queue-prefix: jcpp.uplink.partition
|
||||
```
|
||||
|
||||
### 2. RabbitMQ 队列
|
||||
|
||||
启动应用时会自动创建以下队列:
|
||||
|
||||
```
|
||||
jcpp.uplink.partition.0
|
||||
jcpp.uplink.partition.1
|
||||
jcpp.uplink.partition.2
|
||||
...
|
||||
jcpp.uplink.partition.9
|
||||
```
|
||||
|
||||
每个队列绑定到 `jcpp.uplink.exchange`,routing key 为 `jcpp.uplink.#`
|
||||
|
||||
### 3. 消息格式
|
||||
|
||||
消息体为 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"messageType": "LOGIN",
|
||||
"data": "{\"pileCode\":\"20231212000010\",\"remoteAddress\":\"192.168.1.100\"}"
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `pileCode`:充电桩编码(用于分区计算)
|
||||
- `messageType`:消息类型(LOGIN, HEARTBEAT, GUN_STATUS, REAL_TIME_DATA, TRANSACTION_RECORD 等)
|
||||
- `data`:具体消息内容(JSON 字符串)
|
||||
|
||||
### 4. 支持的消息类型
|
||||
|
||||
| 消息类型 | messageType 值 | 说明 |
|
||||
|---------|---------------|------|
|
||||
| 登录请求 | LOGIN | 充电桩登录 |
|
||||
| 心跳请求 | HEARTBEAT | 心跳保活 |
|
||||
| 枪状态上报 | GUN_STATUS | 充电枪状态变化 |
|
||||
| 充电进度 | REAL_TIME_DATA | 实时充电数据 |
|
||||
| 交易记录 | TRANSACTION_RECORD | 充电完成记录 |
|
||||
| 启动充电 | START_CHARGE | 刷卡启动充电 |
|
||||
| 查询计费 | QUERY_PRICING | 查询计费模板 |
|
||||
| 校验计费 | VERIFY_PRICING | 校验计费模板 |
|
||||
| 会话关闭 | SESSION_CLOSE | 会话关闭事件 |
|
||||
| 远程启动结果 | REMOTE_START_RESULT | 远程启动结果 |
|
||||
| 远程停止结果 | REMOTE_STOP_RESULT | 远程停止结果 |
|
||||
|
||||
## 消息流程
|
||||
|
||||
### 1. JCPP 发送消息
|
||||
|
||||
JCPP 在发送消息时需要:
|
||||
|
||||
1. 将消息序列化为 JSON 格式
|
||||
2. 根据 pileCode 计算分区编号
|
||||
3. 将消息发送到对应的分区队列
|
||||
|
||||
```java
|
||||
// JCPP 端示例代码
|
||||
String pileCode = "20231212000010";
|
||||
int partition = JcppPartitionCalculator.getPartition(pileCode);
|
||||
String queueName = "jcpp.uplink.partition." + partition;
|
||||
|
||||
// 构建消息
|
||||
JcppUplinkMessage message = new JcppUplinkMessage();
|
||||
message.setPileCode(pileCode);
|
||||
message.setMessageType("LOGIN");
|
||||
message.setData("{...}");
|
||||
|
||||
// 发送到指定队列
|
||||
rabbitTemplate.convertAndSend("jcpp.uplink.exchange", queueName, message);
|
||||
```
|
||||
|
||||
### 2. Web 项目消费消息
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class JcppJsonPartitionConsumer {
|
||||
|
||||
@RabbitListener(
|
||||
queues = {
|
||||
"jcpp.uplink.partition.0",
|
||||
"jcpp.uplink.partition.1",
|
||||
// ... 其他分区
|
||||
},
|
||||
concurrency = "1" // 单线程消费保证顺序
|
||||
)
|
||||
public void consumeMessage(JcppUplinkMessage uplinkMessage) {
|
||||
// 处理消息
|
||||
messageHandler.handleUplinkMessage(uplinkMessage);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 消息处理
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class JcppJsonMessageHandlerImpl implements IJcppJsonMessageHandler {
|
||||
|
||||
@Override
|
||||
public void handleUplinkMessage(JcppUplinkMessage message) {
|
||||
// 根据消息类型分发处理
|
||||
switch (message.getMessageType()) {
|
||||
case "LOGIN":
|
||||
handleLogin(message);
|
||||
break;
|
||||
case "HEARTBEAT":
|
||||
handleHeartbeat(message);
|
||||
break;
|
||||
// ... 其他消息类型
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分区算法
|
||||
|
||||
### Hash 计算
|
||||
|
||||
使用 MurmurHash3_128 算法计算分区:
|
||||
|
||||
```java
|
||||
public static int getPartition(String pileCode) {
|
||||
// 使用 MurmurHash3_128 算法
|
||||
long hash = Hashing.murmur3_128()
|
||||
.hashString(pileCode, StandardCharsets.UTF_8)
|
||||
.asLong();
|
||||
|
||||
// 取绝对值并对分区数取模
|
||||
return Math.abs((int) (hash % partitionCount));
|
||||
}
|
||||
```
|
||||
|
||||
### 分区示例
|
||||
|
||||
假设 `partitionCount = 10`:
|
||||
|
||||
| pileCode | Hash 值 | 分区编号 |
|
||||
|----------|---------|---------|
|
||||
| 20231212000010 | 123456789 | 9 |
|
||||
| 20231212000011 | 987654321 | 1 |
|
||||
| 20231212000012 | 456789123 | 3 |
|
||||
|
||||
同一个 pileCode 的所有消息都会路由到同一个分区,由单线程顺序处理。
|
||||
|
||||
## 与原有方案的对比
|
||||
|
||||
### 原有方案(无分区)
|
||||
|
||||
- ❌ 多个消费者并发消费,可能导致消息乱序
|
||||
- ❌ 同一充电桩的消息可能被不同消费者处理
|
||||
- ✅ 实现简单,无需额外配置
|
||||
|
||||
### 新方案(分区消费)
|
||||
|
||||
- ✅ 同一充电桩的消息保证顺序处理
|
||||
- ✅ 每个分区单线程消费,避免并发问题
|
||||
- ✅ 支持水平扩展(增加分区数量)
|
||||
- ⚠️ 需要 JCPP 端配合实现分区路由
|
||||
|
||||
## 监控与运维
|
||||
|
||||
### 1. 日志监控
|
||||
|
||||
```
|
||||
[分区0] 收到消息: pileCode=20231212000010, messageType=LOGIN
|
||||
[分区0] 消息处理完成: pileCode=20231212000010
|
||||
```
|
||||
|
||||
### 2. 性能指标
|
||||
|
||||
- 消息处理延迟
|
||||
- 队列堆积数量
|
||||
- 消费速率
|
||||
- 错误率
|
||||
|
||||
### 3. 告警规则
|
||||
|
||||
- 队列堆积超过阈值
|
||||
- 消息处理失败率超过阈值
|
||||
- 消费延迟超过阈值
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 分区数量
|
||||
|
||||
- 分区数量必须与 JCPP 保持一致
|
||||
- 修改分区数量需要重新创建队列
|
||||
- 建议在系统初始化时确定分区数量,后续不要修改
|
||||
|
||||
### 2. 消息顺序
|
||||
|
||||
- 每个分区单线程消费(`concurrency=1`)
|
||||
- 同一 pileCode 的消息保证顺序
|
||||
- 不同 pileCode 的消息可能并发处理
|
||||
|
||||
### 3. 异常处理
|
||||
|
||||
- JSON 解析失败:记录日志并抛出异常
|
||||
- 业务处理失败:根据业务需求决定是否重试
|
||||
- 建议配置死信队列处理失败消息
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 合理设置分区数量(建议 10-20)
|
||||
- 避免在消息处理中执行耗时操作
|
||||
- 使用 Redis 缓存减少数据库压力
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 消息无法消费
|
||||
|
||||
- 检查队列是否创建成功
|
||||
- 检查队列绑定是否正确
|
||||
- 检查 RabbitMQ 连接是否正常
|
||||
|
||||
### 2. 消息分区错误
|
||||
|
||||
- 检查 Hash 算法是否与 JCPP 一致
|
||||
- 检查分区数量配置是否一致
|
||||
- 查看日志中的分区不匹配警告
|
||||
|
||||
### 3. 消息处理失败
|
||||
|
||||
- 查看异常堆栈信息
|
||||
- 检查业务逻辑是否正确
|
||||
- 检查数据库连接是否正常
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 更新配置
|
||||
|
||||
在 `application-{env}.yml` 中添加 JCPP 配置
|
||||
|
||||
### 2. 启动应用
|
||||
|
||||
```bash
|
||||
mvn clean package -DskipTests
|
||||
java -jar jsowell-admin/target/jsowell-admin.jar --spring.profiles.active=sit
|
||||
```
|
||||
|
||||
### 3. 验证队列创建
|
||||
|
||||
登录 RabbitMQ 管理界面,检查是否创建了 10 个分区队列
|
||||
|
||||
### 4. 发送测试消息
|
||||
|
||||
使用 JCPP 发送测试消息,验证分区路由是否正确
|
||||
|
||||
### 5. 监控运行状态
|
||||
|
||||
查看日志,确认消息正常消费
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Spring AMQP 文档](https://docs.spring.io/spring-amqp/reference/)
|
||||
- [RabbitMQ 分区队列](https://www.rabbitmq.com/partitions.html)
|
||||
- [Guava Hashing](https://github.com/google/guava/wiki/HashingExplained)
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v2.1(JSON 格式)
|
||||
**最后更新**:2025-12-30
|
||||
**维护人员**:jsowell 团队
|
||||
710
doc/JCPP对接进度文档.md
Normal file
710
doc/JCPP对接进度文档.md
Normal file
@@ -0,0 +1,710 @@
|
||||
# JCPP 充电桩平台对接进度文档
|
||||
|
||||
> 文档创建时间:2025-12-30
|
||||
> 最后更新时间:2025-12-30
|
||||
> 对接平台:JCPP(Java Charging Pile Platform)
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
### 1.1 对接目标
|
||||
将万车充运营管理平台与 JCPP 充电桩平台进行对接,实现充电桩设备的统一管理和充电业务的互联互通。
|
||||
|
||||
### 1.2 技术架构
|
||||
- **通信方式**:RabbitMQ 消息队列
|
||||
- **消息格式**:JSON
|
||||
- **协议版本**:yunkuaichongV150
|
||||
- **Exchange**:`jcpp.uplink.exchange`(上行消息)、`jcpp.downlink.exchange`(下行消息)
|
||||
|
||||
### 1.3 模块位置
|
||||
- **核心代码**:`jsowell-pile` 模块
|
||||
- **包路径**:`com.jsowell.pile.jcpp`
|
||||
- **配置类**:`JcppConfig`、`JcppRabbitConfig`
|
||||
|
||||
---
|
||||
|
||||
## 二、已完成功能
|
||||
|
||||
### 2.1 基础架构搭建 ✅
|
||||
|
||||
#### 2.1.1 RabbitMQ 配置
|
||||
- [x] 创建 `JcppRabbitConfig` 配置类
|
||||
- [x] 定义上行消息 Exchange:`jcpp.uplink.exchange`
|
||||
- [x] 定义 9 个消息队列和对应的 Routing Key
|
||||
- [x] 配置队列绑定关系
|
||||
|
||||
**队列列表**:
|
||||
| 队列名称 | Routing Key | 用途 |
|
||||
|---------|-------------|------|
|
||||
| `jcpp.uplink.login` | `jcpp.uplink.login` | 充电桩登录 |
|
||||
| `jcpp.uplink.heartbeat` | `jcpp.uplink.heartbeat` | 心跳消息 |
|
||||
| `jcpp.uplink.startCharge` | `jcpp.uplink.startCharge` | 刷卡启动充电 |
|
||||
| `jcpp.uplink.realTimeData` | `jcpp.uplink.realTimeData` | 实时充电数据 |
|
||||
| `jcpp.uplink.transaction` | `jcpp.uplink.transaction` | 交易记录 |
|
||||
| `jcpp.uplink.gunStatus` | `jcpp.uplink.gunStatus` | 枪状态上报 |
|
||||
| `jcpp.uplink.pricing` | `jcpp.uplink.pricing.#` | 计费模板查询 |
|
||||
| `jcpp.uplink.remoteResult` | `jcpp.uplink.remoteResult.#` | 远程操作结果 |
|
||||
| `jcpp.uplink.sessionClose` | `jcpp.uplink.sessionClose` | 会话关闭 |
|
||||
|
||||
#### 2.1.2 DTO 数据传输对象
|
||||
- [x] `JcppUplinkMessage`:上行消息统一封装
|
||||
- [x] `JcppDownlinkRequest`:下行请求封装
|
||||
- [x] `JcppDownlinkCommand`:下行命令封装
|
||||
- [x] `JcppLoginData`:登录数据
|
||||
- [x] `JcppStartChargeData`:启动充电数据
|
||||
- [x] `JcppRealTimeData`:实时数据
|
||||
- [x] `JcppTransactionData`:交易记录数据
|
||||
- [x] `JcppPricingModel`:计费模板数据
|
||||
- [x] `JcppRemoteStartResultData`:远程启动结果数据
|
||||
- [x] `JcppSessionInfo`:会话信息
|
||||
|
||||
#### 2.1.3 常量定义
|
||||
- [x] `JcppConstants`:定义队列名称、Routing Key、消息类型等常量
|
||||
|
||||
---
|
||||
|
||||
### 2.2 上行消息处理(JCPP → 万车充)✅
|
||||
|
||||
#### 2.2.1 充电桩登录 ✅
|
||||
**消费者**:`JcppLoginConsumer`
|
||||
**队列**:`jcpp.uplink.login`
|
||||
**功能**:
|
||||
- [x] 接收充电桩登录消息
|
||||
- [x] 验证充电桩是否存在于系统中
|
||||
- [x] 发送登录应答(通过 `IJcppDownlinkService.sendLoginAck()`)
|
||||
- [x] 更新充电桩在线状态(已注释,待确认业务逻辑)
|
||||
|
||||
**状态**:✅ 已完成基础功能
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.2 心跳消息 ✅
|
||||
**消费者**:`JcppHeartbeatConsumer`
|
||||
**队列**:`jcpp.uplink.heartbeat`
|
||||
**功能**:
|
||||
- [x] 接收充电桩心跳消息
|
||||
- [x] 更新充电桩最后活跃时间到 Redis(避免频繁写数据库)
|
||||
- [x] 设置 Redis 过期时间为 180 秒
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.3 刷卡启动充电 ✅
|
||||
**消费者**:`JcppStartChargeConsumer`
|
||||
**队列**:`jcpp.uplink.startCharge`
|
||||
**功能**:
|
||||
- [x] 接收刷卡启动充电请求
|
||||
- [x] 验证充电桩状态
|
||||
- [x] 查询授权卡信息(`PileAuthCard`)
|
||||
- [x] 验证卡状态和密码(密码验证已注释)
|
||||
- [x] 查询会员钱包余额
|
||||
- [x] 检查白名单(免费充电)
|
||||
- [x] 验证余额是否充足
|
||||
- [x] 生成交易流水号(`tradeNo`)
|
||||
- [x] 创建充电订单(`OrderBasicInfo`)
|
||||
- [x] 发送鉴权结果应答
|
||||
|
||||
**状态**:✅ 已完成核心功能
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.4 实时充电数据 ✅
|
||||
**消费者**:`JcppRealTimeDataConsumer`
|
||||
**队列**:`jcpp.uplink.realTimeData`
|
||||
**功能**:
|
||||
- [x] 接收实时充电数据(电压、电流、SOC、充电量、充电金额等)
|
||||
- [x] 将实时数据缓存到 Redis(5 分钟过期)
|
||||
- [x] 根据 `tradeNo` 查询订单
|
||||
- [x] 更新订单实时数据(充电量、金额、时长)
|
||||
- [x] 更新枪状态为充电中
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.5 交易记录(订单结算)✅
|
||||
**消费者**:`JcppTransactionConsumer`
|
||||
**队列**:`jcpp.uplink.transaction`
|
||||
**功能**:
|
||||
- [x] 接收充电完成的交易记录
|
||||
- [x] 根据 `tradeNo` 查询订单
|
||||
- [x] 幂等性检查(避免重复处理)
|
||||
- [x] 更新订单状态为"充电完成"
|
||||
- [x] 更新订单充电数据(开始时间、结束时间、充电量、金额、停止原因)
|
||||
- [x] 更新枪状态为空闲
|
||||
- [x] 发送交易记录应答
|
||||
- [ ] 触发结算流程(已注释,待实现)
|
||||
|
||||
**状态**:✅ 基础功能已完成,⚠️ 结算流程待对接
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.6 枪状态上报 ✅
|
||||
**消费者**:`JcppGunStatusConsumer`
|
||||
**队列**:`jcpp.uplink.gunStatus`
|
||||
**功能**:
|
||||
- [x] 接收充电枪状态变化消息
|
||||
- [x] 状态映射(JCPP 状态 → 系统状态)
|
||||
- `IDLE` → `1`(空闲)
|
||||
- `INSERTED` → `2`(占用未充电)
|
||||
- `CHARGING` → `3`(充电中)
|
||||
- `CHARGE_COMPLETE` → `2`(充电完成未拔枪)
|
||||
- `FAULT` → `255`(故障)
|
||||
- `UNKNOWN` → `0`(离网)
|
||||
- [x] 更新枪状态到数据库
|
||||
- [x] 记录故障信息(日志)
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.7 计费模板查询 ✅
|
||||
**消费者**:`JcppPricingConsumer`
|
||||
**队列**:`jcpp.uplink.pricing`
|
||||
**功能**:
|
||||
- [x] 处理计费模板查询请求(`QUERY_PRICING`)
|
||||
- [x] 根据充电桩编码查询关联的计费模板
|
||||
- [x] 将计费模板转换为 JCPP 格式(`PricingModelConverter`)
|
||||
- [x] 发送计费模板查询应答
|
||||
- [x] 处理计费模板校验请求(`VERIFY_PRICING`)
|
||||
- [x] 发送计费模板校验应答
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.8 远程操作结果 ✅
|
||||
**消费者**:`JcppRemoteResultConsumer`
|
||||
**队列**:`jcpp.uplink.remoteResult`
|
||||
**功能**:
|
||||
- [x] 处理远程启动结果(`REMOTE_START_RESULT`)
|
||||
- 启动成功:更新订单状态为"充电中",更新枪状态
|
||||
- 启动失败:更新订单状态为"已取消",记录失败原因
|
||||
- [x] 处理远程停止结果(`REMOTE_STOP_RESULT`)
|
||||
- 记录停止结果日志
|
||||
- 等待交易记录消息进行最终结算
|
||||
- [ ] 启动失败时触发退款流程(已注释,待实现)
|
||||
|
||||
**状态**:✅ 基础功能已完成,⚠️ 退款流程待实现
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.9 会话关闭 ✅
|
||||
**消费者**:`JcppSessionCloseConsumer`
|
||||
**队列**:`jcpp.uplink.sessionClose`
|
||||
**功能**:
|
||||
- [x] 接收充电桩会话关闭消息
|
||||
- [x] 更新充电桩离线状态(已注释)
|
||||
- [x] 更新所有枪状态为离线
|
||||
- [ ] 查询正在充电的订单并标记为异常(待实现)
|
||||
|
||||
**状态**:✅ 基础功能已完成,⚠️ 异常订单处理待实现
|
||||
|
||||
---
|
||||
|
||||
### 2.3 下行消息发送(万车充 → JCPP)✅
|
||||
|
||||
#### 2.3.1 下行服务接口
|
||||
**服务接口**:`IJcppDownlinkService`
|
||||
**实现类**:`JcppDownlinkServiceImpl`
|
||||
|
||||
**已实现方法**:
|
||||
- [x] `sendLoginAck()`:发送登录应答
|
||||
- [x] `sendStartChargeAck()`:发送启动充电鉴权应答
|
||||
- [x] `sendTransactionRecordAck()`:发送交易记录应答
|
||||
- [x] `sendQueryPricingAck()`:发送计费模板查询应答
|
||||
- [x] `sendVerifyPricingAck()`:发送计费模板校验应答
|
||||
- [x] `sendRemoteStart()`:发送远程启动充电命令
|
||||
- [x] `sendRemoteStop()`:发送远程停止充电命令
|
||||
|
||||
**状态**:✅ 已完成核心下行命令
|
||||
|
||||
---
|
||||
|
||||
### 2.4 远程充电控制 ✅
|
||||
|
||||
#### 2.4.1 远程充电服务
|
||||
**服务接口**:`IJcppRemoteChargeService`
|
||||
**实现类**:`JcppRemoteChargeServiceImpl`
|
||||
**Controller**:`JcppRemoteChargeController`(路径:`/jcpp/remote`)
|
||||
|
||||
**功能**:
|
||||
- [x] 远程启动充电(`POST /jcpp/remote/start`)
|
||||
- 验证充电桩和枪状态
|
||||
- 验证会员余额
|
||||
- 创建充电订单
|
||||
- 发送远程启动命令到 JCPP
|
||||
- [x] 远程停止充电(`POST /jcpp/remote/stop`)
|
||||
- 查询订单状态
|
||||
- 发送远程停止命令到 JCPP
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
### 2.5 工具类 ✅
|
||||
|
||||
#### 2.5.1 计费模板转换器
|
||||
**类名**:`PricingModelConverter`
|
||||
**功能**:
|
||||
- [x] 将系统计费模板(`PileBillingTemplate`)转换为 JCPP 格式(`JcppPricingModel`)
|
||||
- [x] 支持分时电价和服务费配置
|
||||
- [x] 单元测试:`PricingModelConverterTest`
|
||||
|
||||
**状态**:✅ 已完成并测试
|
||||
|
||||
---
|
||||
|
||||
## 三、已修复的问题
|
||||
|
||||
### 3.1 消息反序列化错误(第一次修复)✅
|
||||
|
||||
**问题描述**:
|
||||
消费 JCPP 发来的消息时报错:
|
||||
```
|
||||
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException:
|
||||
Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)
|
||||
```
|
||||
|
||||
**原因分析**:
|
||||
JCPP 发送的消息中,`data` 字段是一个 **JSON 字符串**,而不是 JSON 对象。但在 `JcppUplinkMessage` 中定义为 `Object` 类型,导致 Jackson 反序列化时尝试将 JSON 字符串解析为对象,从而失败。
|
||||
|
||||
**解决方案**:
|
||||
1. 修改 `JcppUplinkMessage.data` 字段类型从 `Object` 改为 `String`
|
||||
2. 修改所有消费者中的数据解析逻辑:
|
||||
- 原代码:`JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));`
|
||||
- 修改后:`JSONObject data = JSON.parseObject(uplinkMessage.getData());`
|
||||
|
||||
**修复时间**:2025-12-30
|
||||
**状态**:✅ 已修复
|
||||
|
||||
---
|
||||
|
||||
### 3.2 RabbitMQ 消息转换器冲突(第二次修复)✅
|
||||
|
||||
**问题描述**:
|
||||
修复第一个问题后,仍然出现反序列化错误:
|
||||
```
|
||||
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException:
|
||||
Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)
|
||||
at [Source: (String)"{"pileCode":"20231212000010","messageType":"LOGIN","data":"{\n \"messageIdMSB\": ...
|
||||
```
|
||||
|
||||
**原因分析**:
|
||||
消费者方法签名是 `public void handleLogin(String message)`,但 RabbitMQ 配置了 `Jackson2JsonMessageConverter`。当 RabbitMQ 接收到 JSON 对象消息时:
|
||||
1. `Jackson2JsonMessageConverter` 尝试将 JSON 对象反序列化为 `String` 类型
|
||||
2. Jackson 无法将 JSON 对象转换为 String,导致 `MismatchedInputException`
|
||||
|
||||
**根本原因**:
|
||||
- 消费者期望接收 `String` 类型参数
|
||||
- RabbitMQ 配置了 `Jackson2JsonMessageConverter`,会自动进行 JSON 反序列化
|
||||
- 两者冲突导致反序列化失败
|
||||
|
||||
**解决方案**:
|
||||
将所有消费者的方法参数从 `String message` 改为 `JcppUplinkMessage uplinkMessage`,让 Jackson 自动反序列化为对象。
|
||||
|
||||
**修改内容**:
|
||||
1. 修改所有消费者的方法签名:
|
||||
- 原代码:`public void handleXxx(String message)`
|
||||
- 修改后:`public void handleXxx(JcppUplinkMessage uplinkMessage)`
|
||||
|
||||
2. 简化消息解析逻辑:
|
||||
- 删除:`JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);`
|
||||
- 直接使用:`uplinkMessage` 参数
|
||||
|
||||
3. 优化日志输出:
|
||||
- 原代码:`log.info("收到 JCPP 登录消息: {}", message);`
|
||||
- 修改后:`log.info("收到 JCPP 登录消息: pileCode={}, messageType={}", uplinkMessage.getPileCode(), uplinkMessage.getMessageType());`
|
||||
|
||||
**修改文件**:
|
||||
- [x] `JcppLoginConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppHeartbeatConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppStartChargeConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppRealTimeDataConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppTransactionConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppGunStatusConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppPricingConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppRemoteResultConsumer.java`:修改方法签名和日志
|
||||
- [x] `JcppSessionCloseConsumer.java`:修改方法签名和日志
|
||||
|
||||
**优势**:
|
||||
1. 代码更简洁:不需要手动解析 JSON 字符串
|
||||
2. 性能更好:减少一次 JSON 解析操作
|
||||
3. 类型安全:编译时就能发现类型错误
|
||||
4. 符合 Spring AMQP 最佳实践:利用消息转换器自动反序列化
|
||||
|
||||
**修复时间**:2025-12-30
|
||||
**状态**:✅ 已修复
|
||||
|
||||
---
|
||||
|
||||
## 四、待完成功能
|
||||
|
||||
### 4.1 订单结算流程 ⚠️
|
||||
|
||||
**优先级**:高
|
||||
**描述**:
|
||||
在 `JcppTransactionConsumer` 中,交易记录处理完成后需要触发订单结算流程,包括:
|
||||
- 计算实际费用(电费 + 服务费)
|
||||
- 扣除会员余额或赠金
|
||||
- 执行分账(商户、平台、站点等)
|
||||
- 发放积分奖励
|
||||
- 生成清算账单
|
||||
|
||||
**待实现**:
|
||||
```java
|
||||
// TODO: 触发结算流程
|
||||
// orderBasicInfoService.realTimeOrderSplit(order.getId());
|
||||
```
|
||||
|
||||
**相关类**:
|
||||
- `OrderBasicInfoServiceImpl.realTimeOrderSplit()`
|
||||
- `StationSplitConfigService`
|
||||
- `OrderSplitRecordService`
|
||||
- `PointsRewardProducer`
|
||||
|
||||
---
|
||||
|
||||
### 4.2 远程启动失败退款 ⚠️
|
||||
|
||||
**优先级**:中
|
||||
**描述**:
|
||||
在 `JcppRemoteResultConsumer` 中,如果远程启动失败且用户已预付费,需要触发退款流程。
|
||||
|
||||
**待实现**:
|
||||
```java
|
||||
// TODO: 如果已预付费,触发退款流程
|
||||
```
|
||||
|
||||
**相关类**:
|
||||
- `MemberWalletInfoService`
|
||||
- `MemberWalletLogService`
|
||||
- 第三方支付退款接口(微信、支付宝)
|
||||
|
||||
---
|
||||
|
||||
### 4.3 会话关闭异常订单处理 ⚠️
|
||||
|
||||
**优先级**:中
|
||||
**描述**:
|
||||
在 `JcppSessionCloseConsumer` 中,当充电桩会话关闭时,需要查询是否有正在充电的订单,如果有则标记为异常结束。
|
||||
|
||||
**待实现**:
|
||||
```java
|
||||
// TODO: 查询是否有正在充电的订单,如果有则标记为异常
|
||||
// 可以调用 OrderBasicInfoService 查询并处理
|
||||
```
|
||||
|
||||
**相关类**:
|
||||
- `OrderBasicInfoService`
|
||||
- `OrderAbnormalRecordService`
|
||||
|
||||
---
|
||||
|
||||
### 4.4 充电桩在线状态管理 ⚠️
|
||||
|
||||
**优先级**:低
|
||||
**描述**:
|
||||
目前在登录和会话关闭时更新充电桩在线状态的代码已注释,需要确认业务逻辑后启用。
|
||||
|
||||
**待确认**:
|
||||
- 是否需要实时更新充电桩在线状态到数据库?
|
||||
- 还是仅通过 Redis 心跳判断在线状态?
|
||||
|
||||
**相关代码**:
|
||||
- `JcppLoginConsumer.java:56-59`(已注释)
|
||||
- `JcppSessionCloseConsumer.java:59`(已注释)
|
||||
|
||||
---
|
||||
|
||||
### 4.5 枪故障信息持久化 ⚠️
|
||||
|
||||
**优先级**:低
|
||||
**描述**:
|
||||
在 `JcppGunStatusConsumer` 中,当接收到枪故障信息时,目前仅记录日志,需要将故障信息保存到数据库或发送告警。
|
||||
|
||||
**待实现**:
|
||||
```java
|
||||
// TODO: 可以将故障信息保存到数据库或发送告警
|
||||
```
|
||||
|
||||
**相关表**:
|
||||
- `pile_connector_info`(可能需要添加故障信息字段)
|
||||
- 或创建新表 `pile_connector_fault_record`
|
||||
|
||||
---
|
||||
|
||||
### 4.6 消息服务接口 ⚠️
|
||||
|
||||
**优先级**:低
|
||||
**描述**:
|
||||
`IJcppMessageService` 和 `JcppMessageServiceImpl` 已创建但功能未完善,可能用于消息查询、重发等功能。
|
||||
|
||||
**待实现**:
|
||||
- 消息历史查询
|
||||
- 消息重发机制
|
||||
- 消息统计分析
|
||||
|
||||
---
|
||||
|
||||
## 五、测试情况
|
||||
|
||||
### 5.1 单元测试
|
||||
- [x] `PricingModelConverterTest`:计费模板转换器测试
|
||||
- [ ] 其他消费者单元测试(待补充)
|
||||
|
||||
### 5.2 集成测试
|
||||
- [ ] 充电桩登录流程测试
|
||||
- [ ] 刷卡启动充电流程测试
|
||||
- [ ] 远程启动充电流程测试
|
||||
- [ ] 充电过程实时数据测试
|
||||
- [ ] 充电完成结算流程测试
|
||||
- [ ] 异常场景测试(网络断开、超时等)
|
||||
|
||||
---
|
||||
|
||||
## 六、部署配置
|
||||
|
||||
### 6.1 RabbitMQ 配置
|
||||
|
||||
**配置文件**:`application-{env}.yml`
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST}
|
||||
port: 5672
|
||||
username: ${RABBITMQ_USERNAME}
|
||||
password: ${RABBITMQ_PASSWORD}
|
||||
virtual-host: /
|
||||
listener:
|
||||
simple:
|
||||
acknowledge-mode: auto # 自动应答模式
|
||||
concurrency: 1
|
||||
max-concurrency: 20
|
||||
prefetch: 1
|
||||
retry:
|
||||
enabled: true
|
||||
max-attempts: 3
|
||||
initial-interval: 1s
|
||||
max-interval: 10s
|
||||
multiplier: 2.0
|
||||
```
|
||||
|
||||
### 6.2 JCPP 配置
|
||||
|
||||
**配置类**:`JcppConfig`
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class JcppConfig {
|
||||
// JCPP 平台相关配置
|
||||
// 如:API 地址、认证信息等
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
### 7.1 消息幂等性
|
||||
- 所有消费者都需要做幂等性检查,避免重复处理
|
||||
- 目前 `JcppTransactionConsumer` 已实现幂等性检查
|
||||
- 其他消费者需要根据业务场景补充
|
||||
|
||||
### 7.2 事务管理
|
||||
- 涉及数据库操作的消费者需要添加 `@Transactional` 注解
|
||||
- 目前 `JcppStartChargeConsumer`、`JcppTransactionConsumer`、`JcppRemoteResultConsumer` 已添加
|
||||
|
||||
### 7.3 异常处理
|
||||
- 所有消费者都需要捕获异常并记录日志
|
||||
- 避免异常导致消息丢失或重复消费
|
||||
|
||||
### 7.4 性能优化
|
||||
- 实时数据和心跳消息使用 Redis 缓存,避免频繁写数据库
|
||||
- 考虑使用批量更新减少数据库压力
|
||||
|
||||
---
|
||||
|
||||
## 八、后续计划
|
||||
|
||||
### 8.1 短期计划(1-2 周)
|
||||
1. 完成订单结算流程对接
|
||||
2. 实现远程启动失败退款功能
|
||||
3. 完善会话关闭异常订单处理
|
||||
4. 补充单元测试和集成测试
|
||||
|
||||
### 8.2 中期计划(1 个月)
|
||||
1. 优化消息处理性能
|
||||
2. 完善监控和告警机制
|
||||
3. 补充故障信息持久化功能
|
||||
4. 完善消息服务接口
|
||||
|
||||
### 8.3 长期计划(3 个月)
|
||||
1. 支持更多 JCPP 协议功能
|
||||
2. 实现消息重发和补偿机制
|
||||
3. 优化数据统计和报表功能
|
||||
4. 完善运维工具和管理界面
|
||||
|
||||
---
|
||||
|
||||
## 九、联系人
|
||||
|
||||
- **开发负责人**:待补充
|
||||
- **测试负责人**:待补充
|
||||
- **运维负责人**:待补充
|
||||
|
||||
---
|
||||
|
||||
## 十、变更记录
|
||||
|
||||
| 日期 | 版本 | 修改人 | 修改内容 |
|
||||
|------|------|--------|----------|
|
||||
| 2025-12-30 | v1.0 | Claude | 创建文档,记录当前对接进度 |
|
||||
| 2025-12-30 | v1.1 | Claude | 修复消息反序列化错误(第一次) |
|
||||
| 2025-12-30 | v1.2 | Claude | 修复 RabbitMQ 消息转换器冲突(第二次) |
|
||||
| 2025-12-30 | v2.1 | Claude | 实现 JSON 消息分区消费方案 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、JSON 消息分区消费方案(v2.1 新增)
|
||||
|
||||
### 11.1 方案概述
|
||||
|
||||
为了确保同一充电桩的消息按顺序处理,实现了基于 JSON 格式的分区消费方案:
|
||||
|
||||
- **消息格式**:保持原有 JSON 格式,无需 Protobuf
|
||||
- **分区策略**:基于 pileCode 的 Hash 分区
|
||||
- **分区数量**:10 个分区(可配置)
|
||||
- **Hash 算法**:MurmurHash3_128(与 JCPP 保持一致)
|
||||
- **消费模式**:每个分区单线程消费,保证顺序
|
||||
|
||||
### 11.2 核心组件
|
||||
|
||||
#### 11.2.1 分区计算器
|
||||
|
||||
**类名**:`JcppPartitionCalculator`
|
||||
**功能**:
|
||||
- 使用 MurmurHash3_128 算法计算分区
|
||||
- 根据 pileCode 计算分区编号(0 到 partitionCount-1)
|
||||
- 提供队列名称生成方法
|
||||
|
||||
**核心代码**:
|
||||
```java
|
||||
public static int getPartition(String pileCode) {
|
||||
long hash = Hashing.murmur3_128()
|
||||
.hashString(pileCode, StandardCharsets.UTF_8)
|
||||
.asLong();
|
||||
return Math.abs((int) (hash % partitionCount));
|
||||
}
|
||||
```
|
||||
|
||||
#### 11.2.2 分区队列配置
|
||||
|
||||
**类名**:`JcppPartitionQueueConfig`
|
||||
**功能**:
|
||||
- 创建 10 个分区队列(`jcpp.uplink.partition.0` ~ `jcpp.uplink.partition.9`)
|
||||
- 创建 Topic Exchange(`jcpp.uplink.exchange`)
|
||||
- 绑定队列到 Exchange(routing key: `jcpp.uplink.#`)
|
||||
|
||||
#### 11.2.3 JSON 消息消费者
|
||||
|
||||
**类名**:`JcppJsonPartitionConsumer`
|
||||
**功能**:
|
||||
- 监听所有分区队列
|
||||
- 单线程消费(`concurrency=1`)保证顺序
|
||||
- 自动反序列化 JSON 消息为 `JcppUplinkMessage` 对象
|
||||
- 验证分区是否正确
|
||||
- 调用消息处理器处理消息
|
||||
|
||||
#### 11.2.4 消息处理器
|
||||
|
||||
**接口**:`IJcppJsonMessageHandler`
|
||||
**实现类**:`JcppJsonMessageHandlerImpl`
|
||||
**功能**:
|
||||
- 根据消息类型分发处理
|
||||
- 整合原有所有消费者的处理逻辑
|
||||
- 处理登录、心跳、枪状态、充电进度、交易记录、刷卡启动、计费查询、会话关闭、远程操作结果等消息
|
||||
- 复用原有业务逻辑(`PileBasicInfoService`、`OrderBasicInfoService` 等)
|
||||
|
||||
### 11.3 技术栈
|
||||
|
||||
- **Guava**:33.0.0-jre(提供 MurmurHash3_128)
|
||||
- **Spring AMQP**:2.5.14
|
||||
- **RabbitMQ**:5672
|
||||
- **FastJSON2**:2.0.23
|
||||
|
||||
### 11.4 配置说明
|
||||
|
||||
**配置文件**:`application-{env}.yml`
|
||||
|
||||
```yaml
|
||||
# JCPP 配置
|
||||
jcpp:
|
||||
rabbitmq:
|
||||
# 分区数量(与 JCPP 保持一致)
|
||||
partition-count: 10
|
||||
# Exchange 名称
|
||||
exchange: jcpp.uplink.exchange
|
||||
# 队列前缀
|
||||
queue-prefix: jcpp.uplink.partition
|
||||
```
|
||||
|
||||
### 11.5 消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"messageType": "LOGIN",
|
||||
"data": "{\"pileCode\":\"20231212000010\",\"remoteAddress\":\"192.168.1.100\"}"
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `pileCode`:充电桩编码(用于分区计算)
|
||||
- `messageType`:消息类型
|
||||
- `data`:具体消息内容(JSON 字符串)
|
||||
|
||||
### 11.6 优势
|
||||
|
||||
1. **消息顺序保证**:同一充电桩的消息按顺序处理
|
||||
2. **简单易用**:保持原有 JSON 格式,无需 Protobuf
|
||||
3. **统一处理**:整合所有消息类型的处理逻辑到一个处理器
|
||||
4. **易于调试**:JSON 格式可读性强,便于排查问题
|
||||
5. **可扩展性**:支持动态调整分区数量
|
||||
|
||||
### 11.7 与原有方案的对比
|
||||
|
||||
| 特性 | 原有方案(无分区) | 新方案(分区消费) |
|
||||
|------|------------------|------------------|
|
||||
| 消息格式 | JSON 字符串 | JSON 字符串 |
|
||||
| 消息顺序 | 不保证 | 保证同一 pileCode 顺序 |
|
||||
| 分区消费 | 不支持 | 支持 |
|
||||
| 并发处理 | 多消费者并发 | 每个分区单线程 |
|
||||
| 代码复杂度 | 多个独立消费者 | 统一消息处理器 |
|
||||
| 易于调试 | 较难 | 较易 |
|
||||
|
||||
### 11.8 部署步骤
|
||||
|
||||
1. **更新配置**:在 `application-{env}.yml` 中添加 JCPP 配置
|
||||
2. **启动应用**:应用会自动创建 10 个分区队列
|
||||
3. **JCPP 端配置**:确保 JCPP 使用相同的 Hash 算法和分区数量
|
||||
4. **验证测试**:发送测试消息,验证分区路由和消息处理
|
||||
|
||||
### 11.9 注意事项
|
||||
|
||||
1. **分区数量**:必须与 JCPP 保持一致,建议在系统初始化时确定,后续不要修改
|
||||
2. **Hash 算法**:必须使用 MurmurHash3_128,与 JCPP 保持一致
|
||||
3. **消息顺序**:每个分区单线程消费(`concurrency=1`),不要修改此配置
|
||||
4. **异常处理**:JSON 解析失败会抛出异常,建议配置死信队列
|
||||
5. **性能监控**:监控队列堆积、消费延迟、错误率等指标
|
||||
|
||||
### 11.10 相关文档
|
||||
|
||||
详细使用说明请参考:[JCPP JSON 消息分区消费方案](./JCPP_JSON消息分区消费方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 十、变更记录
|
||||
359
doc/JCPP项目配合实现Prompt.md
Normal file
359
doc/JCPP项目配合实现Prompt.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# JCPP 项目充电桩数据同步接口实现 Prompt
|
||||
|
||||
> 请将以下内容复制给 JCPP 项目的 Claude Code 执行
|
||||
|
||||
---
|
||||
|
||||
## 任务概述
|
||||
|
||||
需要在 JCPP 项目中实现充电桩数据同步接口,用于接收来自 Web 项目的充电桩和充电枪数据。
|
||||
|
||||
## 背景说明
|
||||
|
||||
Web 项目(MySQL)中维护了充电桩的主数据,现在需要将这些数据同步到 JCPP 项目(PostgreSQL)中,用于登录鉴权等操作。
|
||||
|
||||
### 数据流向
|
||||
```
|
||||
Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
|
||||
```
|
||||
|
||||
### 关键差异
|
||||
1. **主键类型**:Web 使用 int,JCPP 使用 uuid
|
||||
2. **station_id 类型**:Web 是 int,JCPP 是 uuid(需要映射)
|
||||
3. **字段名称**:sn → pile_code, name → pile_name
|
||||
|
||||
---
|
||||
|
||||
## 需要实现的功能
|
||||
|
||||
### 1. 充电桩同步接口
|
||||
|
||||
**接口路径**:`POST /api/sync/piles`
|
||||
|
||||
**重要说明**:
|
||||
- ✅ 所有充电桩的 `station_id` 统一使用固定值:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- Web 项目的原始 station_id 保存在 `additionalInfo.webStationId` 中便于追溯
|
||||
|
||||
**请求格式**:
|
||||
```json
|
||||
{
|
||||
"piles": [
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"pileName": "1号充电桩",
|
||||
"protocol": "yunkuaichongV150",
|
||||
"brand": "特来电",
|
||||
"model": "AC-7KW",
|
||||
"manufacturer": "特来电",
|
||||
"type": "OPERATION",
|
||||
"additionalInfo": {
|
||||
"webPileId": 6844,
|
||||
"webStationId": 123,
|
||||
"businessType": "1",
|
||||
"secretKey": "abc123",
|
||||
"longitude": "116.404",
|
||||
"latitude": "39.915",
|
||||
"iccid": "89860123456789012345"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `pileCode`:充电桩编码(对应 Web 的 sn),唯一标识
|
||||
- `pileName`:充电桩名称(对应 Web 的 name)
|
||||
- `protocol`:软件协议(对应 Web 的 software_protocol)
|
||||
- `brand`:品牌(可为空)
|
||||
- `model`:型号(可为空)
|
||||
- `manufacturer`:制造商(可为空)
|
||||
- `type`:类型,枚举值:
|
||||
- `OPERATION`:运营桩(对应 Web 的 business_type = "1")
|
||||
- `PERSONAL`:个人桩(对应 Web 的 business_type = "2")
|
||||
- `additionalInfo`:附加信息(JSON 格式),包含:
|
||||
- `webPileId`:Web 项目的充电桩 ID
|
||||
- `webStationId`:Web 项目的充电站 ID(原始 int 值)
|
||||
- 其他 Web 项目的字段
|
||||
|
||||
**响应格式**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "同步成功",
|
||||
"results": [
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"pileId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"success": true,
|
||||
"message": "创建成功"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**处理逻辑**:
|
||||
1. 遍历 `piles` 数组
|
||||
2. 对于每个充电桩:
|
||||
- 根据 `pileCode` 查询 `t_pile` 表,判断是否已存在
|
||||
- **station_id 统一使用固定值**:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 如果充电桩已存在,执行 UPDATE 操作
|
||||
- 如果充电桩不存在,执行 INSERT 操作(生成新的 uuid)
|
||||
- 将 `additionalInfo` 存储为 jsonb 类型
|
||||
3. 返回每个充电桩的处理结果
|
||||
|
||||
**注意事项**:
|
||||
- 使用事务保证数据一致性
|
||||
- pile_code 必须唯一(已有唯一索引)
|
||||
- 所有充电桩的 station_id 都使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
|
||||
---
|
||||
|
||||
### 2. 充电枪同步接口
|
||||
|
||||
**接口路径**:`POST /api/sync/guns`
|
||||
|
||||
**重要说明**:
|
||||
- ✅ 所有充电枪的 `station_id` 统一使用固定值:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
|
||||
**请求格式**:
|
||||
```json
|
||||
{
|
||||
"guns": [
|
||||
{
|
||||
"gunCode": "2023121200001001",
|
||||
"gunName": "1号枪",
|
||||
"gunNo": "01",
|
||||
"pileCode": "20231212000010",
|
||||
"additionalInfo": {
|
||||
"webGunId": 30060,
|
||||
"status": "1",
|
||||
"parkNo": "A01"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `gunCode`:充电枪编码(对应 Web 的 pile_connector_code),唯一标识
|
||||
- `gunName`:充电枪名称(对应 Web 的 name)
|
||||
- `gunNo`:枪号(从 gunCode 提取最后 2 位)
|
||||
- `pileCode`:所属充电桩编码(对应 Web 的 pile_sn)
|
||||
- `additionalInfo`:附加信息(JSON 格式),包含 Web 项目的其他字段
|
||||
|
||||
**响应格式**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "同步成功",
|
||||
"results": [
|
||||
{
|
||||
"gunCode": "2023121200001001",
|
||||
"gunId": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"success": true,
|
||||
"message": "创建成功"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**处理逻辑**:
|
||||
1. 遍历 `guns` 数组
|
||||
2. 对于每个充电枪:
|
||||
- 根据 `gunCode` 查询 `t_gun` 表,判断是否已存在
|
||||
- 根据 `pileCode` 查询 `t_pile` 表,获取 `pile_id` (uuid)
|
||||
- 如果充电桩不存在,记录错误并跳过该充电枪
|
||||
- **station_id 统一使用固定值**:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 如果充电枪已存在,执行 UPDATE 操作
|
||||
- 如果充电枪不存在,执行 INSERT 操作(生成新的 uuid)
|
||||
3. 返回每个充电枪的处理结果
|
||||
|
||||
**注意事项**:
|
||||
- 充电桩必须先于充电枪同步
|
||||
- gun_code 必须唯一(已有唯一索引)
|
||||
- (pile_id, gun_no) 组合必须唯一(已有唯一索引)
|
||||
- 所有充电枪的 station_id 都使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
|
||||
---
|
||||
|
||||
## 实现建议
|
||||
|
||||
### 技术栈
|
||||
- **框架**:Spring Boot
|
||||
- **数据库**:PostgreSQL 17
|
||||
- **ORM**:JPA / MyBatis
|
||||
- **JSON 处理**:Jackson / Gson
|
||||
|
||||
### 代码结构建议
|
||||
|
||||
```
|
||||
src/main/java/com/jcpp/
|
||||
├── controller/
|
||||
│ └── SyncController.java # 同步接口 Controller
|
||||
├── service/
|
||||
│ ├── PileSyncService.java # 充电桩同步服务接口
|
||||
│ └── GunSyncService.java # 充电枪同步服务接口
|
||||
├── service/impl/
|
||||
│ ├── PileSyncServiceImpl.java
|
||||
│ └── GunSyncServiceImpl.java
|
||||
├── entity/
|
||||
│ ├── Pile.java # t_pile 实体
|
||||
│ └── Gun.java # t_gun 实体
|
||||
├── repository/
|
||||
│ ├── PileRepository.java
|
||||
│ └── GunRepository.java
|
||||
└── dto/
|
||||
├── PileSyncDTO.java # 充电桩同步 DTO
|
||||
├── GunSyncDTO.java # 充电枪同步 DTO
|
||||
├── SyncRequest.java # 同步请求
|
||||
└── SyncResponse.java # 同步响应
|
||||
```
|
||||
|
||||
### 关键代码示例
|
||||
|
||||
**PileSyncService 接口**:
|
||||
```java
|
||||
public interface PileSyncService {
|
||||
/**
|
||||
* 同步充电桩数据
|
||||
* @param piles 充电桩列表
|
||||
* @return 同步结果
|
||||
*/
|
||||
SyncResponse syncPiles(List<PileSyncDTO> piles);
|
||||
}
|
||||
```
|
||||
|
||||
**处理逻辑伪代码**:
|
||||
```java
|
||||
@Transactional
|
||||
public SyncResponse syncPiles(List<PileSyncDTO> piles) {
|
||||
List<SyncResult> results = new ArrayList<>();
|
||||
|
||||
// 固定的 station_id
|
||||
UUID FIXED_STATION_ID = UUID.fromString("88bca8da-cdbf-6587-aecc-75784838c501");
|
||||
|
||||
for (PileSyncDTO dto : piles) {
|
||||
try {
|
||||
// 1. 查询充电桩是否存在
|
||||
Pile existingPile = pileRepository.findByPileCode(dto.getPileCode());
|
||||
|
||||
if (existingPile != null) {
|
||||
// 更新
|
||||
existingPile.setPileName(dto.getPileName());
|
||||
existingPile.setProtocol(dto.getProtocol());
|
||||
existingPile.setStationId(FIXED_STATION_ID); // 使用固定值
|
||||
existingPile.setBrand(dto.getBrand());
|
||||
existingPile.setModel(dto.getModel());
|
||||
existingPile.setManufacturer(dto.getManufacturer());
|
||||
existingPile.setType(dto.getType());
|
||||
existingPile.setAdditionalInfo(dto.getAdditionalInfo());
|
||||
existingPile.setUpdatedTime(new Date());
|
||||
pileRepository.save(existingPile);
|
||||
results.add(SyncResult.success(dto.getPileCode(), existingPile.getId(), "更新成功"));
|
||||
} else {
|
||||
// 创建
|
||||
Pile newPile = new Pile();
|
||||
newPile.setId(UUID.randomUUID());
|
||||
newPile.setPileCode(dto.getPileCode());
|
||||
newPile.setPileName(dto.getPileName());
|
||||
newPile.setProtocol(dto.getProtocol());
|
||||
newPile.setStationId(FIXED_STATION_ID); // 使用固定值
|
||||
newPile.setBrand(dto.getBrand());
|
||||
newPile.setModel(dto.getModel());
|
||||
newPile.setManufacturer(dto.getManufacturer());
|
||||
newPile.setType(dto.getType());
|
||||
newPile.setAdditionalInfo(dto.getAdditionalInfo());
|
||||
newPile.setCreatedTime(new Date());
|
||||
pileRepository.save(newPile);
|
||||
results.add(SyncResult.success(dto.getPileCode(), newPile.getId(), "创建成功"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
results.add(SyncResult.fail(dto.getPileCode(), e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
return SyncResponse.build(results);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 单元测试
|
||||
- 测试 station_id 映射查询
|
||||
- 测试充电桩创建
|
||||
- 测试充电桩更新
|
||||
- 测试充电枪创建
|
||||
- 测试充电枪更新
|
||||
|
||||
### 2. 集成测试
|
||||
- 测试完整的同步流程
|
||||
- 测试异常场景(映射不存在、充电桩不存在等)
|
||||
- 测试并发同步
|
||||
|
||||
### 3. 性能测试
|
||||
- 测试批量同步性能(100、500、1000 条数据)
|
||||
- 测试数据库连接池配置
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **事务处理**
|
||||
- 每批数据使用一个事务
|
||||
- 单个充电桩失败不影响其他充电桩
|
||||
|
||||
2. **错误处理**
|
||||
- 记录详细的错误信息
|
||||
- 返回明确的错误原因
|
||||
|
||||
3. **日志记录**
|
||||
- 记录同步开始和结束时间
|
||||
- 记录成功和失败的数量
|
||||
- 记录详细的错误日志
|
||||
|
||||
4. **性能优化**
|
||||
- 使用批量查询减少数据库访问
|
||||
- 合理设置数据库连接池大小
|
||||
- 考虑使用缓存优化映射查询
|
||||
|
||||
5. **安全性**
|
||||
- 添加接口鉴权
|
||||
- 验证请求数据的合法性
|
||||
- 防止 SQL 注入
|
||||
|
||||
---
|
||||
|
||||
## 交付物
|
||||
|
||||
1. **代码**
|
||||
- Controller、Service、Repository、Entity、DTO 等完整代码
|
||||
- 单元测试代码
|
||||
|
||||
2. **接口文档**
|
||||
- Swagger / OpenAPI 文档
|
||||
- 接口调用示例
|
||||
|
||||
3. **部署说明**
|
||||
- 配置项说明
|
||||
- 部署步骤
|
||||
|
||||
---
|
||||
|
||||
## 联调准备
|
||||
|
||||
完成实现后,请提供:
|
||||
1. 接口地址(如:http://jcpp-server:8080/api/sync)
|
||||
2. 测试账号(如果需要鉴权)
|
||||
3. 测试数据示例
|
||||
|
||||
**重要提醒**:
|
||||
- 确保 JCPP 数据库中已存在 station_id 为 `88bca8da-cdbf-6587-aecc-75784838c501` 的充电站记录
|
||||
- 如果不存在,需要先创建该充电站记录
|
||||
|
||||
---
|
||||
|
||||
## 问题反馈
|
||||
|
||||
如有任何问题或需要澄清的地方,请及时反馈。
|
||||
272
doc/Web项目充电桩同步功能实现总结.md
Normal file
272
doc/Web项目充电桩同步功能实现总结.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Web 项目充电桩数据同步功能实现总结
|
||||
|
||||
> 完成时间:2025-12-30
|
||||
> 状态:✅ Web 项目部分已完成,等待 JCPP 项目配合
|
||||
|
||||
---
|
||||
|
||||
## 一、已完成功能
|
||||
|
||||
### 1. DTO 数据传输对象 ✅
|
||||
|
||||
**位置**:`jsowell-pile/src/main/java/com/jsowell/pile/jcpp/dto/sync/`
|
||||
|
||||
| 文件名 | 说明 |
|
||||
|--------|------|
|
||||
| `JcppPileSyncDTO.java` | 充电桩同步数据 DTO |
|
||||
| `JcppGunSyncDTO.java` | 充电枪同步数据 DTO |
|
||||
| `JcppSyncRequest.java` | 同步请求 DTO |
|
||||
| `JcppSyncResponse.java` | 同步响应 DTO |
|
||||
| `JcppSyncResult.java` | 单个同步结果 DTO |
|
||||
|
||||
### 2. 数据库表和 Mapper ✅
|
||||
|
||||
**建表 SQL**:`sql/jcpp_sync_record.sql`
|
||||
|
||||
**Domain 实体**:`jsowell-pile/src/main/java/com/jsowell/pile/domain/JcppSyncRecord.java`
|
||||
|
||||
**Mapper 接口**:`jsowell-pile/src/main/java/com/jsowell/pile/mapper/JcppSyncRecordMapper.java`
|
||||
|
||||
**Mapper XML**:`jsowell-pile/src/main/resources/mapper/pile/JcppSyncRecordMapper.xml`
|
||||
|
||||
### 3. 同步服务 ✅
|
||||
|
||||
**服务接口**:`jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/IJcppPileSyncService.java`
|
||||
|
||||
**服务实现**:`jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/impl/JcppPileSyncServiceImpl.java`
|
||||
|
||||
**核心功能**:
|
||||
- ✅ 全量同步充电桩数据
|
||||
- ✅ 增量同步充电桩数据(基于 update_time)
|
||||
- ✅ 单个充电桩同步
|
||||
- ✅ 数据格式转换(Web → JCPP)
|
||||
- ✅ 调用 JCPP 同步接口
|
||||
- ✅ 同步记录管理
|
||||
|
||||
### 4. Controller 接口 ✅
|
||||
|
||||
**控制器**:`jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/JcppPileSyncController.java`
|
||||
|
||||
**API 接口**:
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| POST | `/jcpp/sync/full` | 全量同步 | `jcpp:sync:full` |
|
||||
| POST | `/jcpp/sync/incremental` | 增量同步 | `jcpp:sync:incremental` |
|
||||
| POST | `/jcpp/sync/pile/{pileSn}` | 单个充电桩同步 | `jcpp:sync:single` |
|
||||
| GET | `/jcpp/sync/records` | 查询同步记录列表 | `jcpp:sync:list` |
|
||||
| GET | `/jcpp/sync/records/{id}` | 查询同步记录详情 | `jcpp:sync:query` |
|
||||
|
||||
### 5. 配置项 ✅
|
||||
|
||||
**配置文件**:`jsowell-admin/src/main/resources/application-sit.yml`
|
||||
|
||||
```yaml
|
||||
jcpp:
|
||||
sync:
|
||||
# JCPP 同步接口地址
|
||||
api-url: http://localhost:8080/api/sync
|
||||
# 批量同步大小
|
||||
batch-size: 100
|
||||
# 超时时间(毫秒)
|
||||
timeout: 60000
|
||||
# 是否启用自动增量同步
|
||||
auto-sync-enabled: false
|
||||
# 自动同步间隔(分钟)
|
||||
auto-sync-interval: 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心实现逻辑
|
||||
|
||||
### 1. 数据转换逻辑
|
||||
|
||||
#### 充电桩数据转换
|
||||
```java
|
||||
Web (pile_basic_info) → JCPP (t_pile)
|
||||
- sn → pileCode
|
||||
- name → pileName
|
||||
- software_protocol → protocol
|
||||
- business_type → type (1→OPERATION, 2→PERSONAL)
|
||||
- 其他字段 → additionalInfo (JSON)
|
||||
- station_id → 固定值 '88bca8da-cdbf-6587-aecc-75784838c501'
|
||||
```
|
||||
|
||||
#### 充电枪数据转换
|
||||
```java
|
||||
Web (pile_connector_info) → JCPP (t_gun)
|
||||
- pile_connector_code → gunCode
|
||||
- name → gunName
|
||||
- pile_sn → pileCode
|
||||
- 最后2位 → gunNo
|
||||
- 其他字段 → additionalInfo (JSON)
|
||||
- station_id → 固定值 '88bca8da-cdbf-6587-aecc-75784838c501'
|
||||
```
|
||||
|
||||
### 2. 同步流程
|
||||
|
||||
#### 全量同步
|
||||
1. 创建同步记录(状态:RUNNING)
|
||||
2. 查询所有未删除的充电桩和充电枪
|
||||
3. 转换数据格式
|
||||
4. 调用 JCPP 同步接口
|
||||
- 先同步充电桩(POST /api/sync/piles)
|
||||
- 再同步充电枪(POST /api/sync/guns)
|
||||
5. 更新同步记录(状态:SUCCESS/FAILED)
|
||||
|
||||
#### 增量同步
|
||||
1. 获取上次同步时间(参数或查询最后一次成功记录)
|
||||
2. 查询 update_time > lastSyncTime 的数据
|
||||
3. 其他步骤同全量同步
|
||||
|
||||
#### 单个充电桩同步
|
||||
1. 查询指定充电桩及其所有充电枪
|
||||
2. 转换数据格式
|
||||
3. 调用 JCPP 同步接口
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
- ✅ 接口调用异常捕获
|
||||
- ✅ 单个数据失败不影响其他数据
|
||||
- ✅ 详细的错误信息记录
|
||||
- ✅ 同步记录状态管理
|
||||
|
||||
---
|
||||
|
||||
## 三、使用说明
|
||||
|
||||
### 1. 数据库初始化
|
||||
|
||||
执行建表 SQL:
|
||||
```bash
|
||||
mysql -u username -p database_name < sql/jcpp_sync_record.sql
|
||||
```
|
||||
|
||||
### 2. 配置 JCPP 接口地址
|
||||
|
||||
修改 `application-sit.yml`:
|
||||
```yaml
|
||||
jcpp:
|
||||
sync:
|
||||
api-url: http://jcpp-server:8080/api/sync # 修改为实际地址
|
||||
```
|
||||
|
||||
### 3. API 调用示例
|
||||
|
||||
#### 全量同步
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/jcpp/sync/full \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
#### 增量同步
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/jcpp/sync/incremental?lastSyncTime=2025-12-30 10:00:00" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
#### 单个充电桩同步
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/jcpp/sync/pile/20231212000010 \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
#### 查询同步记录
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/jcpp/sync/records?pageNum=1&pageSize=10" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、待完成事项
|
||||
|
||||
### 1. JCPP 项目需要实现 ⏳
|
||||
|
||||
请参考文档:`doc/JCPP项目配合实现Prompt.md`
|
||||
|
||||
需要实现的接口:
|
||||
- [ ] POST /api/sync/piles - 充电桩同步接口
|
||||
- [ ] POST /api/sync/guns - 充电枪同步接口
|
||||
|
||||
**重要提醒**:
|
||||
- 所有充电桩和充电枪的 station_id 统一使用固定值:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 确保 JCPP 数据库中已存在该 station_id 的充电站记录
|
||||
|
||||
### 2. 联调测试 ⏳
|
||||
|
||||
- [ ] 测试全量同步功能
|
||||
- [ ] 测试增量同步功能
|
||||
- [ ] 测试单个充电桩同步
|
||||
- [ ] 测试异常场景(网络异常、数据异常等)
|
||||
- [ ] 性能测试(大批量数据同步)
|
||||
|
||||
### 3. 优化项(可选)
|
||||
|
||||
- [ ] 添加按 update_time 查询的 Mapper 方法(提升增量同步性能)
|
||||
- [ ] 添加重试机制(网络异常时自动重试)
|
||||
- [ ] 添加同步进度查询(异步任务)
|
||||
- [ ] 添加定时自动同步(基于配置)
|
||||
- [ ] 添加同步数据校验(数据完整性检查)
|
||||
|
||||
---
|
||||
|
||||
## 五、技术要点
|
||||
|
||||
### 1. 固定 station_id 方案
|
||||
|
||||
**优势**:
|
||||
- ✅ 简化实现,无需维护映射表
|
||||
- ✅ 减少接口调用次数
|
||||
- ✅ 降低系统复杂度
|
||||
|
||||
**注意事项**:
|
||||
- Web 的原始 station_id 保存在 additionalInfo.webStationId 中
|
||||
- 便于后续追溯和数据分析
|
||||
|
||||
### 2. 数据转换
|
||||
|
||||
**business_type 映射**:
|
||||
- "1" → "OPERATION"(运营桩)
|
||||
- "2" → "PERSONAL"(个人桩)
|
||||
|
||||
**gun_no 提取**:
|
||||
- pile_connector_code: "2023121200001001"
|
||||
- gun_no: "01"(最后 2 位)
|
||||
|
||||
### 3. 同步记录
|
||||
|
||||
**状态**:
|
||||
- RUNNING:进行中
|
||||
- SUCCESS:成功
|
||||
- FAILED:失败
|
||||
|
||||
**统计信息**:
|
||||
- 总数、成功数、失败数(充电桩和充电枪分别统计)
|
||||
- 错误信息列表
|
||||
|
||||
---
|
||||
|
||||
## 六、相关文档
|
||||
|
||||
- [充电桩数据同步需求.md](./充电桩数据同步需求.md) - 原始需求
|
||||
- [充电桩数据同步实现计划.md](./充电桩数据同步实现计划.md) - 详细实现计划
|
||||
- [JCPP项目配合实现Prompt.md](./JCPP项目配合实现Prompt.md) - 给 JCPP 项目的实现指南
|
||||
- [JCPP对接进度文档.md](./JCPP对接进度文档.md) - JCPP 整体对接进度
|
||||
|
||||
---
|
||||
|
||||
## 七、问题反馈
|
||||
|
||||
如有任何问题,请及时反馈:
|
||||
1. 接口调用异常
|
||||
2. 数据转换错误
|
||||
3. 性能问题
|
||||
4. 其他技术问题
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2025-12-30
|
||||
**维护人员**:jsowell 团队
|
||||
464
doc/充电桩数据同步实现计划.md
Normal file
464
doc/充电桩数据同步实现计划.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# 充电桩数据同步实现计划
|
||||
|
||||
> 创建时间:2025-12-30
|
||||
> 任务状态:规划中
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
将 Web 项目(MySQL)中的充电桩数据同步到 JCPP 项目(PostgreSQL)中,用于登录鉴权等操作。
|
||||
|
||||
### 1.1 数据流向
|
||||
```
|
||||
Web 项目 (MySQL) → JCPP 项目 (PostgreSQL)
|
||||
```
|
||||
|
||||
### 1.2 同步方式
|
||||
- **全量同步**:同步所有充电桩和充电枪数据
|
||||
- **增量同步**:只同步新增或修改的数据(基于 update_time)
|
||||
|
||||
---
|
||||
|
||||
## 二、表结构映射分析
|
||||
|
||||
### 2.1 充电桩表映射
|
||||
|
||||
| Web 字段 (pile_basic_info) | JCPP 字段 (t_pile) | 映射说明 |
|
||||
|---------------------------|-------------------|---------|
|
||||
| id (int) | - | Web 主键,不同步 |
|
||||
| sn (varchar(20)) | pile_code (varchar(255)) | 充电桩编码,唯一标识 |
|
||||
| name (varchar(32)) | pile_name (varchar(255)) | 充电桩名称 |
|
||||
| software_protocol (varchar(20)) | protocol (varchar(255)) | 软件协议 |
|
||||
| station_id (int) | station_id (uuid) | ✅ 统一使用固定值 '88bca8da-cdbf-6587-aecc-75784838c501' |
|
||||
| model_id (int) | model (varchar(255)) | 型号,需要查询转换 |
|
||||
| - | brand (varchar(255)) | 品牌,可为空 |
|
||||
| - | manufacturer (varchar(255)) | 制造商,可为空 |
|
||||
| business_type (char(5)) | type (varchar(16)) | 经营类型映射 |
|
||||
| 其他字段 | additional_info (jsonb) | 存储为 JSON |
|
||||
|
||||
### 2.2 充电枪表映射
|
||||
|
||||
| Web 字段 (pile_connector_info) | JCPP 字段 (t_gun) | 映射说明 |
|
||||
|-------------------------------|------------------|---------|
|
||||
| id (int) | - | Web 主键,不同步 |
|
||||
| pile_connector_code (varchar(20)) | gun_code (varchar(255)) | 充电枪编码,唯一标识 |
|
||||
| name (varchar(20)) | gun_name (varchar(255)) | 充电枪名称 |
|
||||
| pile_sn (varchar(20)) | pile_id (uuid) | ⚠️ 需要通过 pile_code 查询 pile_id |
|
||||
| - | gun_no (varchar(255)) | 枪号,从 pile_connector_code 提取 |
|
||||
| - | station_id (uuid) | ✅ 统一使用固定值 '88bca8da-cdbf-6587-aecc-75784838c501' |
|
||||
|
||||
### 2.3 关键问题
|
||||
|
||||
#### 问题1:station_id 类型映射(int → uuid)
|
||||
**解决方案**:
|
||||
- ✅ 统一使用固定的 UUID 值:`88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 所有充电桩都使用这个固定的 station_id
|
||||
- Web 的原始 station_id 保存在 additional_info 中便于追溯
|
||||
|
||||
#### 问题2:pile_id 获取
|
||||
**解决方案**:
|
||||
- 先同步充电桩,获取返回的 pile_id
|
||||
- 再同步充电枪时,通过 pile_code 查询 pile_id
|
||||
|
||||
#### 问题3:gun_no 提取
|
||||
**解决方案**:
|
||||
- pile_connector_code 格式为 "桩号+枪号"(如:20231212000010**01**)
|
||||
- 提取最后 2 位作为 gun_no
|
||||
|
||||
---
|
||||
|
||||
## 三、实现方案
|
||||
|
||||
### 3.1 Web 项目实现(本项目)
|
||||
|
||||
#### 3.1.1 数据传输对象(DTO)
|
||||
|
||||
**文件位置**:`jsowell-pile/src/main/java/com/jsowell/pile/jcpp/dto/sync/`
|
||||
|
||||
1. **JcppPileSyncDTO** - 充电桩同步数据
|
||||
```java
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"pileName": "1号充电桩",
|
||||
"protocol": "yunkuaichongV150",
|
||||
"brand": "特来电",
|
||||
"model": "AC-7KW",
|
||||
"manufacturer": "特来电",
|
||||
"type": "OPERATION", // OPERATION-运营桩, PERSONAL-个人桩
|
||||
"additionalInfo": {
|
||||
"webPileId": 6844,
|
||||
"webStationId": 123, // 保存 Web 的原始 station_id
|
||||
"businessType": "1",
|
||||
"secretKey": "abc123",
|
||||
"longitude": "116.404",
|
||||
"latitude": "39.915",
|
||||
"iccid": "89860123456789012345"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:所有充电桩的 station_id 统一使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
|
||||
2. **JcppGunSyncDTO** - 充电枪同步数据
|
||||
```java
|
||||
{
|
||||
"gunCode": "2023121200001001",
|
||||
"gunName": "1号枪",
|
||||
"gunNo": "01",
|
||||
"pileCode": "20231212000010",
|
||||
"additionalInfo": {
|
||||
"webGunId": 30060,
|
||||
"status": "1",
|
||||
"parkNo": "A01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **JcppSyncRequest** - 同步请求
|
||||
```java
|
||||
{
|
||||
"syncType": "FULL", // FULL-全量, INCREMENTAL-增量
|
||||
"lastSyncTime": "2025-12-30 10:00:00", // 增量同步时使用
|
||||
"piles": [...],
|
||||
"guns": [...]
|
||||
}
|
||||
```
|
||||
|
||||
4. **JcppSyncResponse** - 同步响应
|
||||
```java
|
||||
{
|
||||
"success": true,
|
||||
"message": "同步成功",
|
||||
"totalPiles": 100,
|
||||
"successPiles": 98,
|
||||
"failedPiles": 2,
|
||||
"totalGuns": 200,
|
||||
"successGuns": 195,
|
||||
"failedGuns": 5,
|
||||
"errors": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.2 服务接口
|
||||
|
||||
**文件位置**:`jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/`
|
||||
|
||||
1. **IJcppPileSyncService** - 充电桩同步服务接口
|
||||
```java
|
||||
public interface IJcppPileSyncService {
|
||||
/**
|
||||
* 全量同步充电桩数据到 JCPP
|
||||
*/
|
||||
JcppSyncResponse syncAllPiles();
|
||||
|
||||
/**
|
||||
* 增量同步充电桩数据到 JCPP
|
||||
* @param lastSyncTime 上次同步时间
|
||||
*/
|
||||
JcppSyncResponse syncIncrementalPiles(Date lastSyncTime);
|
||||
|
||||
/**
|
||||
* 同步单个充电桩
|
||||
*/
|
||||
boolean syncSinglePile(String pileSn);
|
||||
}
|
||||
```
|
||||
|
||||
2. **JcppPileSyncServiceImpl** - 实现类
|
||||
- 查询 Web 数据库
|
||||
- 转换数据格式
|
||||
- 调用 JCPP 接口发送数据
|
||||
- 处理同步结果
|
||||
|
||||
#### 3.1.3 Controller 接口
|
||||
|
||||
**文件位置**:`jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/`
|
||||
|
||||
**JcppPileSyncController**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/jcpp/sync")
|
||||
public class JcppPileSyncController {
|
||||
|
||||
/**
|
||||
* 全量同步充电桩数据
|
||||
* POST /jcpp/sync/full
|
||||
*/
|
||||
@PostMapping("/full")
|
||||
public AjaxResult syncFull();
|
||||
|
||||
/**
|
||||
* 增量同步充电桩数据
|
||||
* POST /jcpp/sync/incremental
|
||||
* @param lastSyncTime 上次同步时间(可选,默认取上次记录)
|
||||
*/
|
||||
@PostMapping("/incremental")
|
||||
public AjaxResult syncIncremental(@RequestParam(required = false) Date lastSyncTime);
|
||||
|
||||
/**
|
||||
* 同步单个充电桩
|
||||
* POST /jcpp/sync/pile/{pileSn}
|
||||
*/
|
||||
@PostMapping("/pile/{pileSn}")
|
||||
public AjaxResult syncSinglePile(@PathVariable String pileSn);
|
||||
|
||||
/**
|
||||
* 查询同步记录
|
||||
* GET /jcpp/sync/records
|
||||
*/
|
||||
@GetMapping("/records")
|
||||
public TableDataInfo getSyncRecords();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.4 同步记录表
|
||||
|
||||
**表名**:`jcpp_sync_record`
|
||||
```sql
|
||||
CREATE TABLE `jcpp_sync_record` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`sync_type` varchar(20) NOT NULL COMMENT '同步类型(FULL-全量;INCREMENTAL-增量)',
|
||||
`sync_status` varchar(20) NOT NULL COMMENT '同步状态(RUNNING-进行中;SUCCESS-成功;FAILED-失败)',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`total_piles` int(11) DEFAULT 0 COMMENT '总充电桩数',
|
||||
`success_piles` int(11) DEFAULT 0 COMMENT '成功充电桩数',
|
||||
`failed_piles` int(11) DEFAULT 0 COMMENT '失败充电桩数',
|
||||
`total_guns` int(11) DEFAULT 0 COMMENT '总充电枪数',
|
||||
`success_guns` int(11) DEFAULT 0 COMMENT '成功充电枪数',
|
||||
`failed_guns` int(11) DEFAULT 0 COMMENT '失败充电枪数',
|
||||
`error_message` text COMMENT '错误信息',
|
||||
`create_by` varchar(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_sync_type` (`sync_type`),
|
||||
KEY `idx_start_time` (`start_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='JCPP 充电桩同步记录表';
|
||||
```
|
||||
|
||||
#### 3.1.5 配置项
|
||||
|
||||
**配置文件**:`application-{env}.yml`
|
||||
```yaml
|
||||
jcpp:
|
||||
sync:
|
||||
# JCPP 同步接口地址
|
||||
api-url: http://jcpp-server:8080/api/sync
|
||||
# 批量同步大小
|
||||
batch-size: 100
|
||||
# 超时时间(秒)
|
||||
timeout: 60
|
||||
# 是否启用自动增量同步
|
||||
auto-sync-enabled: false
|
||||
# 自动同步间隔(分钟)
|
||||
auto-sync-interval: 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 JCPP 项目实现(需要配合)
|
||||
|
||||
#### 3.2.1 需要实现的接口
|
||||
|
||||
**接口1:接收充电桩同步数据**
|
||||
```
|
||||
POST /api/sync/piles
|
||||
Content-Type: application/json
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"piles": [
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"pileName": "1号充电桩",
|
||||
"protocol": "yunkuaichongV150",
|
||||
"brand": "特来电",
|
||||
"model": "AC-7KW",
|
||||
"manufacturer": "特来电",
|
||||
"type": "OPERATION",
|
||||
"additionalInfo": {
|
||||
"webStationId": 123 // Web 的原始 station_id
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"message": "同步成功",
|
||||
"results": [
|
||||
{
|
||||
"pileCode": "20231212000010",
|
||||
"pileId": "uuid",
|
||||
"success": true,
|
||||
"message": "创建成功"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:所有充电桩的 station_id 统一使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
|
||||
**接口2:接收充电枪同步数据**
|
||||
```
|
||||
POST /api/sync/guns
|
||||
Content-Type: application/json
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"guns": [
|
||||
{
|
||||
"gunCode": "2023121200001001",
|
||||
"gunName": "1号枪",
|
||||
"gunNo": "01",
|
||||
"pileCode": "20231212000010",
|
||||
"additionalInfo": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"message": "同步成功",
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 数据处理逻辑
|
||||
|
||||
1. **充电桩同步逻辑**
|
||||
- 根据 pile_code 判断是否已存在
|
||||
- 存在则更新,不存在则创建
|
||||
- station_id 统一使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 返回 pile_id (uuid)
|
||||
|
||||
2. **充电枪同步逻辑**
|
||||
- 根据 gun_code 判断是否已存在
|
||||
- 通过 pile_code 查询 pile_id
|
||||
- station_id 统一使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
|
||||
- 存在则更新,不存在则创建
|
||||
|
||||
---
|
||||
|
||||
## 四、实现步骤
|
||||
|
||||
### 阶段一:Web 项目基础实现 ✅
|
||||
|
||||
- [x] 1. 创建 DTO 类(JcppPileSyncDTO、JcppGunSyncDTO、JcppSyncRequest、JcppSyncResponse、JcppSyncResult)
|
||||
- [x] 2. 创建同步记录表(jcpp_sync_record)及 Mapper
|
||||
- [x] 3. 创建 Service 接口和实现类(IJcppPileSyncService、JcppPileSyncServiceImpl)
|
||||
- [x] 4. 创建 Controller 接口(JcppPileSyncController)
|
||||
- [x] 5. 添加配置项(application-sit.yml)
|
||||
|
||||
**已完成文件**:
|
||||
- DTO: `JcppPileSyncDTO.java`, `JcppGunSyncDTO.java`, `JcppSyncRequest.java`, `JcppSyncResponse.java`, `JcppSyncResult.java`
|
||||
- Domain: `JcppSyncRecord.java`
|
||||
- Mapper: `JcppSyncRecordMapper.java`, `JcppSyncRecordMapper.xml`
|
||||
- Service: `IJcppPileSyncService.java`, `JcppPileSyncServiceImpl.java`
|
||||
- Controller: `JcppPileSyncController.java`
|
||||
- SQL: `sql/jcpp_sync_record.sql`
|
||||
- Config: `application-sit.yml` (已添加 jcpp.sync 配置)
|
||||
|
||||
### 阶段二:JCPP 项目配合实现 ⏳
|
||||
|
||||
- [ ] 1. 实现充电桩同步接口(使用固定 station_id)
|
||||
- [ ] 2. 实现充电枪同步接口(使用固定 station_id)
|
||||
|
||||
### 阶段三:联调测试 ⏳
|
||||
|
||||
- [ ] 1. 测试全量同步
|
||||
- [ ] 2. 测试增量同步
|
||||
- [ ] 3. 测试单个充电桩同步
|
||||
- [ ] 4. 测试异常场景
|
||||
- [ ] 5. 性能测试
|
||||
|
||||
### 阶段四:优化完善 ⏳
|
||||
|
||||
- [ ] 1. 添加重试机制
|
||||
- [ ] 2. 添加同步进度查询
|
||||
- [ ] 3. 添加定时自动同步
|
||||
- [ ] 4. 完善错误处理和日志
|
||||
|
||||
---
|
||||
|
||||
## 五、技术要点
|
||||
|
||||
### 5.1 数据转换
|
||||
|
||||
1. **business_type 映射**
|
||||
- Web: "1"-运营桩, "2"-个人桩
|
||||
- JCPP: "OPERATION"-运营桩, "PERSONAL"-个人桩
|
||||
|
||||
2. **gun_no 提取**
|
||||
- pile_connector_code: "2023121200001001"
|
||||
- gun_no: "01" (最后2位)
|
||||
|
||||
3. **additional_info 构建**
|
||||
- 将 Web 的其他字段存储为 JSON
|
||||
- 保留原始 ID 便于追溯
|
||||
|
||||
### 5.2 性能优化
|
||||
|
||||
1. **批量同步**
|
||||
- 每批 100 条数据
|
||||
- 避免一次性加载所有数据
|
||||
|
||||
2. **异步处理**
|
||||
- 全量同步使用异步任务
|
||||
- 返回任务 ID,可查询进度
|
||||
|
||||
3. **增量同步**
|
||||
- 基于 update_time 字段
|
||||
- 记录上次同步时间
|
||||
|
||||
### 5.3 异常处理
|
||||
|
||||
1. **网络异常**
|
||||
- 重试机制(最多3次)
|
||||
- 指数退避策略
|
||||
|
||||
2. **数据异常**
|
||||
- 记录失败数据
|
||||
- 继续处理其他数据
|
||||
|
||||
3. **事务处理**
|
||||
- JCPP 端保证事务一致性
|
||||
- Web 端记录同步状态
|
||||
|
||||
---
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
1. **数据一致性**
|
||||
- 充电桩必须先于充电枪同步
|
||||
- 确保 pile_code 唯一性
|
||||
|
||||
2. **性能考虑**
|
||||
- 全量同步可能耗时较长
|
||||
- 建议在业务低峰期执行
|
||||
|
||||
3. **安全性**
|
||||
- 同步接口需要鉴权
|
||||
- 敏感数据加密传输
|
||||
|
||||
4. **监控告警**
|
||||
- 同步失败告警
|
||||
- 同步耗时监控
|
||||
|
||||
---
|
||||
|
||||
## 七、变更记录
|
||||
|
||||
| 日期 | 版本 | 修改人 | 修改内容 |
|
||||
|------|------|--------|----------|
|
||||
| 2025-12-30 | v1.0 | Claude | 创建文档,制定实现计划 |
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文档
|
||||
|
||||
- [充电桩数据同步需求.md](./充电桩数据同步需求.md)
|
||||
- [JCPP对接进度文档.md](./JCPP对接进度文档.md)
|
||||
111
doc/充电桩数据同步需求.md
Normal file
111
doc/充电桩数据同步需求.md
Normal file
@@ -0,0 +1,111 @@
|
||||
充电桩数据首先是在web项目生成的,现在需要把web项目生成的充电桩数据同步到JCPP项目中,用于登录鉴权等操作
|
||||
|
||||
下面是web项目中充电桩相关表结构(注意使用的是mysql 5.7)
|
||||
|
||||
~~~mysql
|
||||
CREATE TABLE `pile_basic_info` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`name` varchar(32) DEFAULT NULL COMMENT '别名',
|
||||
`sn` varchar(20) DEFAULT NULL COMMENT '桩号',
|
||||
`business_type` char(5) DEFAULT NULL COMMENT '经营类型(1-运营桩;2-个人桩)',
|
||||
`secret_key` varchar(10) DEFAULT NULL COMMENT '个人桩密钥',
|
||||
`software_protocol` varchar(20) DEFAULT NULL COMMENT '软件协议(yunkuaichongV150--云快充V1.5;yunkuaichongV160--云快充V1.6;yonglianV1--永联;youdianV1--友电)',
|
||||
`production_date` datetime DEFAULT NULL COMMENT '生产日期',
|
||||
`licence_id` int(11) DEFAULT NULL COMMENT '证书编号',
|
||||
`model_id` int(11) DEFAULT NULL COMMENT '充电桩型号',
|
||||
`sim_id` int(11) DEFAULT NULL COMMENT 'sim卡id',
|
||||
`iccid` varchar(50) DEFAULT NULL COMMENT 'sim卡iccid',
|
||||
`merchant_id` int(11) DEFAULT NULL COMMENT '运营商id',
|
||||
`station_id` int(11) DEFAULT NULL COMMENT '充电站id',
|
||||
`longitude` varchar(30) DEFAULT NULL COMMENT '经度',
|
||||
`latitude` varchar(30) DEFAULT NULL COMMENT '纬度',
|
||||
`vin_flag` char(5) DEFAULT NULL COMMENT '是否支持汽车VIN码识别',
|
||||
`fault_reason` varchar(255) DEFAULT NULL COMMENT '故障原因',
|
||||
`create_by` varchar(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(20) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` char(5) DEFAULT '0' COMMENT '删除标识(0-正常;1-删除)',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_sn` (`sn`) USING BTREE,
|
||||
KEY `idx_station_id` (`station_id`) USING BTREE,
|
||||
KEY `idx_iccid` (`iccid`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6845 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='充电桩基本信息表';
|
||||
|
||||
CREATE TABLE `pile_connector_info` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`name` varchar(20) DEFAULT NULL COMMENT '名称',
|
||||
`pile_sn` varchar(20) DEFAULT NULL COMMENT '所属充电桩sn',
|
||||
`pile_connector_code` varchar(20) DEFAULT NULL COMMENT '充电枪编号,由充电桩SN+01生成',
|
||||
`status` varchar(5) DEFAULT '0' COMMENT '状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 ',
|
||||
`park_no` varchar(5) DEFAULT NULL COMMENT '车位号(推送联联平台所用字段)',
|
||||
`create_by` varchar(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(20) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标识(0-正常;1-删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_pile_sn` (`pile_sn`) USING BTREE,
|
||||
KEY `idx_code` (`pile_connector_code`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=30061 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='充电桩枪口信息表';
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
下面是JCPP项目充电桩相关表结构(注意使用的是postgreSQL 17)
|
||||
|
||||
~~~
|
||||
CREATE TABLE "public"."t_pile" (
|
||||
"id" uuid NOT NULL,
|
||||
"created_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_time" timestamp(6),
|
||||
"additional_info" jsonb,
|
||||
"pile_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"pile_code" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"protocol" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"station_id" uuid NOT NULL,
|
||||
"brand" varchar(255) COLLATE "pg_catalog"."default",
|
||||
"model" varchar(255) COLLATE "pg_catalog"."default",
|
||||
"manufacturer" varchar(255) COLLATE "pg_catalog"."default",
|
||||
"type" varchar(16) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"version" int4 DEFAULT 1,
|
||||
CONSTRAINT "pile_pkey" PRIMARY KEY ("id")
|
||||
)
|
||||
;
|
||||
|
||||
ALTER TABLE "public"."t_pile"
|
||||
OWNER TO "postgres";
|
||||
|
||||
CREATE UNIQUE INDEX "uni_pile_code" ON "public"."t_pile" USING btree (
|
||||
"pile_code" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
|
||||
);
|
||||
|
||||
CREATE TABLE "public"."t_gun" (
|
||||
"id" uuid NOT NULL,
|
||||
"created_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_time" timestamp(6),
|
||||
"additional_info" varchar(255) COLLATE "pg_catalog"."default",
|
||||
"gun_no" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"gun_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"gun_code" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
|
||||
"station_id" uuid NOT NULL,
|
||||
"pile_id" uuid NOT NULL,
|
||||
"version" int4 DEFAULT 1,
|
||||
CONSTRAINT "t_gun_pkey" PRIMARY KEY ("id")
|
||||
)
|
||||
;
|
||||
|
||||
ALTER TABLE "public"."t_gun"
|
||||
OWNER TO "postgres";
|
||||
|
||||
CREATE UNIQUE INDEX "uni_gun_code" ON "public"."t_gun" USING btree (
|
||||
"gun_code" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "uni_gun_pile_gun_no" ON "public"."t_gun" USING btree (
|
||||
"pile_id" "pg_catalog"."uuid_ops" ASC NULLS LAST,
|
||||
"gun_no" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
|
||||
);
|
||||
~~~
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.jsowell.web.controller.jcpp;
|
||||
|
||||
import com.jsowell.common.annotation.Log;
|
||||
import com.jsowell.common.core.controller.BaseController;
|
||||
import com.jsowell.common.core.domain.AjaxResult;
|
||||
import com.jsowell.common.core.page.TableDataInfo;
|
||||
import com.jsowell.common.enums.BusinessType;
|
||||
import com.jsowell.pile.domain.JcppSyncRecord;
|
||||
import com.jsowell.pile.jcpp.dto.sync.JcppSyncResponse;
|
||||
import com.jsowell.pile.jcpp.service.IJcppPileSyncService;
|
||||
import com.jsowell.pile.mapper.JcppSyncRecordMapper;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步控制器
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "JCPP 充电桩同步")
|
||||
@RestController
|
||||
@RequestMapping("/jcpp/sync")
|
||||
public class JcppPileSyncController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IJcppPileSyncService jcppPileSyncService;
|
||||
|
||||
@Autowired
|
||||
private JcppSyncRecordMapper jcppSyncRecordMapper;
|
||||
|
||||
/**
|
||||
* 全量同步充电桩数据
|
||||
*/
|
||||
@ApiOperation("全量同步充电桩数据")
|
||||
@PreAuthorize("@ss.hasPermi('jcpp:sync:full')")
|
||||
@Log(title = "JCPP 充电桩同步", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/full")
|
||||
public AjaxResult syncFull() {
|
||||
try {
|
||||
log.info("开始执行全量同步");
|
||||
JcppSyncResponse response = jcppPileSyncService.syncAllPiles();
|
||||
return AjaxResult.success("全量同步完成", response);
|
||||
} catch (Exception e) {
|
||||
log.error("全量同步失败", e);
|
||||
return AjaxResult.error("全量同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量同步充电桩数据
|
||||
*/
|
||||
@ApiOperation("增量同步充电桩数据")
|
||||
@PreAuthorize("@ss.hasPermi('jcpp:sync:incremental')")
|
||||
@Log(title = "JCPP 充电桩同步", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/incremental")
|
||||
public AjaxResult syncIncremental(
|
||||
@ApiParam("上次同步时间(可选,格式:yyyy-MM-dd HH:mm:ss)")
|
||||
@RequestParam(required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
Date lastSyncTime) {
|
||||
try {
|
||||
log.info("开始执行增量同步,上次同步时间: {}", lastSyncTime);
|
||||
JcppSyncResponse response = jcppPileSyncService.syncIncrementalPiles(lastSyncTime);
|
||||
return AjaxResult.success("增量同步完成", response);
|
||||
} catch (Exception e) {
|
||||
log.error("增量同步失败", e);
|
||||
return AjaxResult.error("增量同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步单个充电桩
|
||||
*/
|
||||
@ApiOperation("同步单个充电桩")
|
||||
@PreAuthorize("@ss.hasPermi('jcpp:sync:single')")
|
||||
@Log(title = "JCPP 充电桩同步", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/pile/{pileSn}")
|
||||
public AjaxResult syncSinglePile(
|
||||
@ApiParam("充电桩编号")
|
||||
@PathVariable String pileSn) {
|
||||
try {
|
||||
log.info("开始同步单个充电桩: {}", pileSn);
|
||||
boolean success = jcppPileSyncService.syncSinglePile(pileSn);
|
||||
if (success) {
|
||||
return AjaxResult.success("同步成功");
|
||||
} else {
|
||||
return AjaxResult.error("同步失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("同步单个充电桩失败: {}", pileSn, e);
|
||||
return AjaxResult.error("同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同步记录列表
|
||||
*/
|
||||
@ApiOperation("查询同步记录列表")
|
||||
@PreAuthorize("@ss.hasPermi('jcpp:sync:list')")
|
||||
@GetMapping("/records")
|
||||
public TableDataInfo getSyncRecords(JcppSyncRecord jcppSyncRecord) {
|
||||
startPage();
|
||||
List<JcppSyncRecord> list = jcppSyncRecordMapper.selectJcppSyncRecordList(jcppSyncRecord);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同步记录详情
|
||||
*/
|
||||
@ApiOperation("查询同步记录详情")
|
||||
@PreAuthorize("@ss.hasPermi('jcpp:sync:query')")
|
||||
@GetMapping("/records/{id}")
|
||||
public AjaxResult getSyncRecordDetail(
|
||||
@ApiParam("同步记录ID")
|
||||
@PathVariable Long id) {
|
||||
JcppSyncRecord record = jcppSyncRecordMapper.selectJcppSyncRecordById(id);
|
||||
return AjaxResult.success(record);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,33 @@ jsowell:
|
||||
# 文件路径 示例( Windows配置D:/jsowell/uploadPath,Linux配置 /home/jsowell/uploadPath)
|
||||
profile: /www/wwwroot/jsowellftp
|
||||
|
||||
# JCPP 配置
|
||||
jcpp:
|
||||
rabbitmq:
|
||||
# 分区数量(与 JCPP 保持一致)
|
||||
partition-count: 10
|
||||
# Exchange 名称
|
||||
exchange: jcpp.uplink.exchange
|
||||
# 队列前缀
|
||||
queue-prefix: jcpp.uplink.partition
|
||||
sync:
|
||||
# JCPP 同步接口地址
|
||||
api-url: http://localhost:8080/api/sync
|
||||
# 批量同步大小
|
||||
batch-size: 100
|
||||
# 超时时间(毫秒)
|
||||
timeout: 60000
|
||||
# 是否启用自动增量同步
|
||||
auto-sync-enabled: false
|
||||
# 自动同步间隔(分钟)
|
||||
auto-sync-interval: 30
|
||||
|
||||
# 数据源配置
|
||||
spring:
|
||||
# redis 配置
|
||||
redis:
|
||||
# 地址
|
||||
host: 192.168.0.32
|
||||
host: 106.14.94.149
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
@@ -35,11 +56,11 @@ spring:
|
||||
druid:
|
||||
# 主库数据源
|
||||
master:
|
||||
url: jdbc:mysql://192.168.0.32:3306/jsowell_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: jsowell_dev
|
||||
# url: jdbc:mysql://192.168.0.32:3306/jsowell_prd_copy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
url: jdbc:mysql://106.14.94.149:3306/jsowell_pre?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: jsowell_pre
|
||||
# url: jdbc:mysql://106.14.94.149:3306/jsowell_prd_copy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
# username: jsowell_prd_copy
|
||||
password: 123456
|
||||
password: Js@160829
|
||||
# 从库数据源
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
@@ -89,7 +110,7 @@ spring:
|
||||
|
||||
# rabbitmq配置 sit
|
||||
rabbitmq:
|
||||
host: 192.168.0.32
|
||||
host: 106.14.94.149
|
||||
port: 5672
|
||||
username: admin
|
||||
password: admin
|
||||
@@ -263,7 +284,7 @@ dubbo:
|
||||
name: wcc-server
|
||||
qosEnable: false
|
||||
registry:
|
||||
address: nacos://192.168.0.32:8848
|
||||
address: nacos://106.14.94.149:8848
|
||||
parameters:
|
||||
namespace: e328faaf-8516-42d0-817a-7406232b3581
|
||||
username: nacos
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -52,7 +51,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("LOGIN")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(loginData)
|
||||
.data(JSON.toJSONString(loginData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -77,7 +76,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("HEARTBEAT")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(heartbeatData)
|
||||
.data(JSON.toJSONString(heartbeatData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -101,7 +100,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("QUERY_PRICING")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(queryData)
|
||||
.data(JSON.toJSONString(queryData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -130,7 +129,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("START_CHARGE")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(startData)
|
||||
.data(JSON.toJSONString(startData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -163,7 +162,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("REAL_TIME_DATA")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(realTimeData)
|
||||
.data(JSON.toJSONString(realTimeData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -196,7 +195,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("TRANSACTION_RECORD")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(transactionData)
|
||||
.data(JSON.toJSONString(transactionData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -224,7 +223,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("REMOTE_START_RESULT")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(resultData)
|
||||
.data(JSON.toJSONString(resultData))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
@@ -245,7 +244,7 @@ public class JcppMessageControllerTest {
|
||||
.pileCode("TEST001")
|
||||
.messageType("INVALID_TYPE")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(new HashMap<>())
|
||||
.data(JSON.toJSONString(new HashMap<>()))
|
||||
.build();
|
||||
|
||||
mockMvc.perform(post("/api/jcpp/uplink")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.jsowell.jcpp;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
|
||||
import com.jsowell.pile.jcpp.util.PricingModelConverter;
|
||||
import com.jsowell.pile.vo.web.BillingDetailVO;
|
||||
@@ -25,17 +26,17 @@ public class PricingModelConverterTest {
|
||||
@Test
|
||||
public void testConvertStandardPricing() {
|
||||
BillingTemplateVO template = new BillingTemplateVO();
|
||||
template.setId(1L);
|
||||
template.setName("标准计费模板");
|
||||
template.setTemplateId("1L");
|
||||
template.setTemplateName("标准计费模板");
|
||||
|
||||
List<BillingDetailVO> details = new ArrayList<>();
|
||||
BillingDetailVO detail = new BillingDetailVO();
|
||||
detail.setTimeType("3"); // 平时
|
||||
detail.setElectricityPrice(new BigDecimal("0.8"));
|
||||
detail.setServicePrice(new BigDecimal("0.4"));
|
||||
detail.setApplyTime("00:00-24:00");
|
||||
detail.setApplyTime(Lists.newArrayList("00:00-24:00"));
|
||||
details.add(detail);
|
||||
template.setBillingDetailList(details);
|
||||
// template.setBillingDetailList(details);
|
||||
|
||||
JcppPricingModel model = PricingModelConverter.convert(template);
|
||||
|
||||
@@ -53,8 +54,8 @@ public class PricingModelConverterTest {
|
||||
@Test
|
||||
public void testConvertPeakValleyPricing() {
|
||||
BillingTemplateVO template = new BillingTemplateVO();
|
||||
template.setId(2L);
|
||||
template.setName("峰谷计费模板");
|
||||
template.setTemplateId("2L");
|
||||
template.setTemplateName("峰谷计费模板");
|
||||
|
||||
List<BillingDetailVO> details = new ArrayList<>();
|
||||
|
||||
@@ -63,7 +64,7 @@ public class PricingModelConverterTest {
|
||||
sharpDetail.setTimeType("1");
|
||||
sharpDetail.setElectricityPrice(new BigDecimal("1.2"));
|
||||
sharpDetail.setServicePrice(new BigDecimal("0.6"));
|
||||
sharpDetail.setApplyTime("10:00-12:00,18:00-20:00");
|
||||
sharpDetail.setApplyTime(Lists.newArrayList("10:00-12:00","18:00-20:00"));
|
||||
details.add(sharpDetail);
|
||||
|
||||
// 峰时
|
||||
@@ -71,7 +72,7 @@ public class PricingModelConverterTest {
|
||||
peakDetail.setTimeType("2");
|
||||
peakDetail.setElectricityPrice(new BigDecimal("1.0"));
|
||||
peakDetail.setServicePrice(new BigDecimal("0.5"));
|
||||
peakDetail.setApplyTime("08:00-10:00,12:00-18:00");
|
||||
peakDetail.setApplyTime(Lists.newArrayList("08:00-10:00","12:00-18:00"));
|
||||
details.add(peakDetail);
|
||||
|
||||
// 平时
|
||||
@@ -79,7 +80,7 @@ public class PricingModelConverterTest {
|
||||
flatDetail.setTimeType("3");
|
||||
flatDetail.setElectricityPrice(new BigDecimal("0.8"));
|
||||
flatDetail.setServicePrice(new BigDecimal("0.4"));
|
||||
flatDetail.setApplyTime("06:00-08:00,20:00-22:00");
|
||||
flatDetail.setApplyTime(Lists.newArrayList("06:00-08:00","20:00-22:00"));
|
||||
details.add(flatDetail);
|
||||
|
||||
// 谷时
|
||||
@@ -87,10 +88,10 @@ public class PricingModelConverterTest {
|
||||
valleyDetail.setTimeType("4");
|
||||
valleyDetail.setElectricityPrice(new BigDecimal("0.4"));
|
||||
valleyDetail.setServicePrice(new BigDecimal("0.2"));
|
||||
valleyDetail.setApplyTime("00:00-06:00,22:00-24:00");
|
||||
valleyDetail.setApplyTime(Lists.newArrayList("00:00-06:00","22:00-24:00"));
|
||||
details.add(valleyDetail);
|
||||
|
||||
template.setBillingDetailList(details);
|
||||
// template.setBillingDetailList(details);
|
||||
|
||||
JcppPricingModel model = PricingModelConverter.convert(template);
|
||||
|
||||
@@ -131,9 +132,9 @@ public class PricingModelConverterTest {
|
||||
@Test
|
||||
public void testConvertTemplateWithoutDetails() {
|
||||
BillingTemplateVO template = new BillingTemplateVO();
|
||||
template.setId(3L);
|
||||
template.setName("无详情模板");
|
||||
template.setBillingDetailList(null);
|
||||
template.setTemplateId("3L");
|
||||
template.setTemplateName("无详情模板");
|
||||
// template.setBillingDetailList(null);
|
||||
|
||||
JcppPricingModel model = PricingModelConverter.convert(template);
|
||||
|
||||
|
||||
@@ -165,6 +165,12 @@
|
||||
<groupId>com.jsowell</groupId>
|
||||
<artifactId>charge-common-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Google Protobuf -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
@@ -187,7 +193,36 @@
|
||||
<encoding>utf-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Protobuf Maven Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.25.1:exe:${os.detected.classifier}</protocArtifact>
|
||||
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
|
||||
<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>
|
||||
<clearOutputDirectory>false</clearOutputDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<extensions>
|
||||
<!-- OS Maven Plugin for platform detection -->
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.jsowell.pile.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.jsowell.common.annotation.Excel;
|
||||
import com.jsowell.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步记录对象 jcpp_sync_record
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class JcppSyncRecord extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 同步类型(FULL-全量;INCREMENTAL-增量)
|
||||
*/
|
||||
@Excel(name = "同步类型")
|
||||
private String syncType;
|
||||
|
||||
/**
|
||||
* 同步状态(RUNNING-进行中;SUCCESS-成功;FAILED-失败)
|
||||
*/
|
||||
@Excel(name = "同步状态")
|
||||
private String syncStatus;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/**
|
||||
* 总充电桩数
|
||||
*/
|
||||
@Excel(name = "总充电桩数")
|
||||
private Integer totalPiles;
|
||||
|
||||
/**
|
||||
* 成功充电桩数
|
||||
*/
|
||||
@Excel(name = "成功充电桩数")
|
||||
private Integer successPiles;
|
||||
|
||||
/**
|
||||
* 失败充电桩数
|
||||
*/
|
||||
@Excel(name = "失败充电桩数")
|
||||
private Integer failedPiles;
|
||||
|
||||
/**
|
||||
* 总充电枪数
|
||||
*/
|
||||
@Excel(name = "总充电枪数")
|
||||
private Integer totalGuns;
|
||||
|
||||
/**
|
||||
* 成功充电枪数
|
||||
*/
|
||||
@Excel(name = "成功充电枪数")
|
||||
private Integer successGuns;
|
||||
|
||||
/**
|
||||
* 失败充电枪数
|
||||
*/
|
||||
@Excel(name = "失败充电枪数")
|
||||
private Integer failedGuns;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.jsowell.pile.jcpp.config;
|
||||
|
||||
import com.jsowell.pile.jcpp.constant.JcppConstants;
|
||||
import com.jsowell.pile.jcpp.util.JcppPartitionCalculator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.TopicExchange;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP Protobuf 消息分区队列配置
|
||||
* 实现基于 messageKey 的分区消费,确保同一充电桩的消息顺序处理
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class JcppPartitionQueueConfig {
|
||||
|
||||
@Value("${jcpp.rabbitmq.partition-count:10}")
|
||||
private int partitionCount;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 设置分区数量到计算器
|
||||
JcppPartitionCalculator.setPartitionCount(partitionCount);
|
||||
log.info("JCPP 分区队列配置初始化完成,分区数量: {}", partitionCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分区队列数组
|
||||
* 每个分区一个队列,用于顺序消费
|
||||
*/
|
||||
@Bean
|
||||
public Queue[] jcppPartitionQueues() {
|
||||
Queue[] queues = new Queue[partitionCount];
|
||||
for (int i = 0; i < partitionCount; i++) {
|
||||
String queueName = JcppPartitionCalculator.getQueueName(i);
|
||||
queues[i] = new Queue(queueName, true, false, false);
|
||||
log.info("创建 JCPP 分区队列: {}", queueName);
|
||||
}
|
||||
return queues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定分区队列到 Exchange
|
||||
* 每个分区队列绑定所有消息类型(jcpp.uplink.#)
|
||||
* 实际分区由 JCPP 在发送消息时通过 header 指定
|
||||
*
|
||||
* 注意:复用 JcppRabbitConfig 中定义的 jcppUplinkExchange Bean
|
||||
*/
|
||||
@Bean
|
||||
public Binding[] jcppPartitionBindings(
|
||||
TopicExchange jcppUplinkExchange,
|
||||
Queue[] jcppPartitionQueues) {
|
||||
|
||||
List<Binding> bindings = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < partitionCount; i++) {
|
||||
// 每个分区队列绑定所有消息类型
|
||||
Binding binding = BindingBuilder
|
||||
.bind(jcppPartitionQueues[i])
|
||||
.to(jcppUplinkExchange)
|
||||
.with("jcpp.uplink.#");
|
||||
|
||||
bindings.add(binding);
|
||||
log.info("绑定分区队列 {} 到 Exchange: {}",
|
||||
jcppPartitionQueues[i].getName(),
|
||||
jcppUplinkExchange.getName());
|
||||
}
|
||||
|
||||
return bindings.toArray(new Binding[0]);
|
||||
}
|
||||
}
|
||||
@@ -50,26 +50,31 @@ public class JcppGunStatusConsumer {
|
||||
}
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_GUN_STATUS)
|
||||
public void handleGunStatus(String message) {
|
||||
public void handleGunStatus(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 枪状态消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("枪状态消息格式错误: {}", message);
|
||||
log.warn("枪状态消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取信息
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("枪状态消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 枪状态消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
|
||||
|
||||
// 从 data 中获取其他信息
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String gunRunStatus = data.getString("gunRunStatus");
|
||||
JSONArray faultMessages = data.getJSONArray("faultMessages");
|
||||
|
||||
if (pileCode == null || gunNo == null) {
|
||||
log.warn("枪状态消息缺少必要字段: {}", message);
|
||||
if (gunNo == null) {
|
||||
log.warn("枪状态消息缺少 gunNo");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +99,8 @@ public class JcppGunStatusConsumer {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 枪状态消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 枪状态消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,20 +28,18 @@ public class JcppHeartbeatConsumer {
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_HEARTBEAT)
|
||||
public void handleHeartbeat(String message) {
|
||||
public void handleHeartbeat(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.debug("心跳消息格式错误: {}", message);
|
||||
log.debug("心跳消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取 pileCode
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.debug("心跳消息缺少 pileCode: {}", message);
|
||||
log.debug("心跳消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,7 +51,8 @@ public class JcppHeartbeatConsumer {
|
||||
log.debug("收到充电桩心跳: pileCode={}", pileCode);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 心跳消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 心跳消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.jsowell.pile.jcpp.consumer;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
|
||||
import com.jsowell.pile.jcpp.service.IJcppJsonMessageHandler;
|
||||
import com.jsowell.pile.jcpp.util.JcppPartitionCalculator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.amqp.support.AmqpHeaders;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* JCPP JSON 消息消费者 - 分区消费
|
||||
* 每个分区单线程消费,保证同一充电桩的消息顺序处理
|
||||
*
|
||||
* 注意:此消费者默认禁用,需要在 JCPP 端配置好分区路由并创建队列后再启用
|
||||
* 启用方法:取消 @Component 注解的注释
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
// @Component // 暂时禁用,等 JCPP 端配置好分区路由并创建队列后再启用
|
||||
public class JcppJsonPartitionConsumer {
|
||||
|
||||
@Autowired
|
||||
private IJcppJsonMessageHandler messageHandler;
|
||||
|
||||
@RabbitListener(
|
||||
queues = {
|
||||
"jcpp.uplink.partition.0",
|
||||
"jcpp.uplink.partition.1",
|
||||
"jcpp.uplink.partition.2",
|
||||
"jcpp.uplink.partition.3",
|
||||
"jcpp.uplink.partition.4",
|
||||
"jcpp.uplink.partition.5",
|
||||
"jcpp.uplink.partition.6",
|
||||
"jcpp.uplink.partition.7",
|
||||
"jcpp.uplink.partition.8",
|
||||
"jcpp.uplink.partition.9"
|
||||
},
|
||||
concurrency = "1" // 每个队列单线程消费保证顺序
|
||||
)
|
||||
public void consumeMessage(JcppUplinkMessage uplinkMessage,
|
||||
@Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
|
||||
@Header(AmqpHeaders.CONSUMER_QUEUE) String queueName) {
|
||||
// 从队列名称提取分区编号
|
||||
int partition = extractPartitionFromQueue(queueName);
|
||||
|
||||
try {
|
||||
String messageKey = uplinkMessage.getPileCode();
|
||||
String messageType = uplinkMessage.getMessageType();
|
||||
|
||||
log.info("[分区{}] 收到消息: pileCode={}, messageType={}, routingKey={}",
|
||||
partition, messageKey, messageType, routingKey);
|
||||
|
||||
// 验证分区是否正确
|
||||
int expectedPartition = JcppPartitionCalculator.getPartition(messageKey);
|
||||
if (expectedPartition != partition) {
|
||||
log.warn("[分区{}] 消息分区不匹配: pileCode={}, 期望分区={}, 实际分区={}",
|
||||
partition, messageKey, expectedPartition, partition);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
messageHandler.handleUplinkMessage(uplinkMessage);
|
||||
|
||||
log.debug("[分区{}] 消息处理完成: pileCode={}", partition, messageKey);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[分区{}] 消息处理失败: pileCode={}",
|
||||
partition, uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
throw new RuntimeException("消息处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队列名称提取分区编号
|
||||
* 队列名称格式: jcpp.uplink.partition.{partition}
|
||||
*/
|
||||
private int extractPartitionFromQueue(String queueName) {
|
||||
try {
|
||||
String[] parts = queueName.split("\\.");
|
||||
return Integer.parseInt(parts[parts.length - 1]);
|
||||
} catch (Exception e) {
|
||||
log.error("无法从队列名称提取分区编号: {}", queueName, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,25 +28,26 @@ public class JcppLoginConsumer {
|
||||
private IJcppDownlinkService jcppDownlinkService;
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_LOGIN)
|
||||
public void handleLogin(String message) {
|
||||
public void handleLogin(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 登录消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("登录消息格式错误: {}", message);
|
||||
log.warn("登录消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取 pileCode
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("登录消息缺少 pileCode: {}", message);
|
||||
log.warn("登录消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 登录消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
|
||||
|
||||
// 解析 data
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
|
||||
// 查询充电桩是否存在
|
||||
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
|
||||
boolean exists = pileInfo != null;
|
||||
@@ -66,7 +67,8 @@ public class JcppLoginConsumer {
|
||||
jcppDownlinkService.sendLoginAck(pileCode, exists);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 登录消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 登录消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,26 +36,28 @@ public class JcppPricingConsumer {
|
||||
private IJcppDownlinkService jcppDownlinkService;
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_PRICING)
|
||||
public void handlePricing(String message) {
|
||||
public void handlePricing(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 计费消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("计费消息格式错误: {}", message);
|
||||
log.warn("计费消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 uplinkMessage 中获取 pileCode 和 messageType
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
String messageType = uplinkMessage.getMessageType();
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("计费消息缺少 pileCode: {}", message);
|
||||
log.warn("计费消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 计费消息: pileCode={}, messageType={}", pileCode, messageType);
|
||||
|
||||
// 解析 data
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
|
||||
// 根据消息类型处理
|
||||
if (JcppConstants.MessageType.QUERY_PRICING.equals(messageType)) {
|
||||
handleQueryPricing(pileCode);
|
||||
@@ -67,7 +69,8 @@ public class JcppPricingConsumer {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 计费消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 计费消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,18 +39,23 @@ public class JcppRealTimeDataConsumer {
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_REAL_TIME_DATA)
|
||||
public void handleRealTimeData(String message) {
|
||||
public void handleRealTimeData(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.debug("实时数据消息格式错误: {}", message);
|
||||
log.debug("实时数据消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取信息
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.debug("实时数据消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取其他信息
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
String outputVoltage = data.getString("outputVoltage");
|
||||
@@ -61,7 +66,7 @@ public class JcppRealTimeDataConsumer {
|
||||
String totalChargingCostYuan = data.getString("totalChargingCostYuan");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.debug("实时数据消息缺少 tradeNo: {}", message);
|
||||
log.debug("实时数据消息缺少 tradeNo");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,7 +106,8 @@ public class JcppRealTimeDataConsumer {
|
||||
log.debug("处理实时数据: tradeNo={}, soc={}, energy={}kWh", tradeNo, soc, totalChargingEnergyKWh);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 实时数据消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 实时数据消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,19 +30,19 @@ public class JcppRemoteResultConsumer {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_REMOTE_RESULT)
|
||||
public void handleRemoteResult(String message) {
|
||||
public void handleRemoteResult(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 远程操作结果消息: {}", message);
|
||||
log.info("收到 JCPP 远程操作结果消息: pileCode={}, messageType={}",
|
||||
uplinkMessage.getPileCode(), uplinkMessage.getMessageType());
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("远程操作结果消息格式错误: {}", message);
|
||||
log.warn("远程操作结果消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
String messageType = uplinkMessage.getMessageType();
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
|
||||
// 根据消息类型处理
|
||||
if (JcppConstants.MessageType.REMOTE_START_RESULT.equals(messageType)) {
|
||||
@@ -54,7 +54,8 @@ public class JcppRemoteResultConsumer {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 远程操作结果消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 远程操作结果消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +63,14 @@ public class JcppRemoteResultConsumer {
|
||||
* 处理远程启动结果
|
||||
*/
|
||||
private void handleRemoteStartResult(JSONObject data) {
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
Boolean success = data.getBoolean("success");
|
||||
String failReason = data.getString("failReason");
|
||||
|
||||
// 从 data 中获取 pileCode 和 gunNo(如果有)
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.warn("远程启动结果缺少 tradeNo");
|
||||
return;
|
||||
@@ -110,11 +113,13 @@ public class JcppRemoteResultConsumer {
|
||||
* 处理远程停止结果
|
||||
*/
|
||||
private void handleRemoteStopResult(JSONObject data) {
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
Boolean success = data.getBoolean("success");
|
||||
String failReason = data.getString("failReason");
|
||||
|
||||
// 从 data 中获取 pileCode 和 gunNo(如果有)
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
log.info("远程停止成功: pileCode={}, gunNo={}", pileCode, gunNo);
|
||||
} else {
|
||||
|
||||
@@ -28,27 +28,27 @@ public class JcppSessionCloseConsumer {
|
||||
private PileConnectorInfoService pileConnectorInfoService;
|
||||
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_SESSION_CLOSE)
|
||||
public void handleSessionClose(String message) {
|
||||
public void handleSessionClose(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 会话关闭消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("会话关闭消息格式错误: {}", message);
|
||||
log.warn("会话关闭消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取信息
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
String reason = data.getString("reason");
|
||||
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("会话关闭消息缺少 pileCode: {}", message);
|
||||
log.warn("会话关闭消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 会话关闭消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
|
||||
|
||||
// 从 data 中获取其他信息
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String reason = data.getString("reason");
|
||||
|
||||
log.info("充电桩会话关闭: pileCode={}, reason={}", pileCode, reason);
|
||||
|
||||
// 1. 更新充电桩离线状态
|
||||
@@ -70,7 +70,8 @@ public class JcppSessionCloseConsumer {
|
||||
// 可以调用 OrderBasicInfoService 查询并处理
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 会话关闭消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 会话关闭消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,28 +54,33 @@ public class JcppStartChargeConsumer {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_START_CHARGE)
|
||||
public void handleStartCharge(String message) {
|
||||
public void handleStartCharge(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 启动充电消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("启动充电消息格式错误: {}", message);
|
||||
log.warn("启动充电消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取信息
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("启动充电消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 启动充电消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
|
||||
|
||||
// 从 data 中获取其他信息
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String startType = data.getString("startType");
|
||||
String cardNo = data.getString("cardNo");
|
||||
Boolean needPassword = data.getBoolean("needPassword");
|
||||
String password = data.getString("password");
|
||||
|
||||
if (pileCode == null || gunNo == null) {
|
||||
log.warn("启动充电消息缺少必要字段: {}", message);
|
||||
if (gunNo == null) {
|
||||
log.warn("启动充电消息缺少 gunNo");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,7 +94,8 @@ public class JcppStartChargeConsumer {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 启动充电消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 启动充电消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,20 +38,25 @@ public class JcppTransactionConsumer {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RabbitListener(queues = JcppConstants.QUEUE_TRANSACTION)
|
||||
public void handleTransaction(String message) {
|
||||
public void handleTransaction(JcppUplinkMessage uplinkMessage) {
|
||||
try {
|
||||
log.info("收到 JCPP 交易记录消息: {}", message);
|
||||
|
||||
// 解析消息
|
||||
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
|
||||
// 验证消息
|
||||
if (uplinkMessage == null || uplinkMessage.getData() == null) {
|
||||
log.warn("交易记录消息格式错误: {}", message);
|
||||
log.warn("交易记录消息格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 data 中获取信息
|
||||
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
|
||||
String pileCode = data.getString("pileCode");
|
||||
// 从 uplinkMessage 中获取 pileCode
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
if (pileCode == null || pileCode.isEmpty()) {
|
||||
log.warn("交易记录消息缺少 pileCode");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("收到 JCPP 交易记录消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
|
||||
|
||||
// 从 data 中获取其他信息
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
Long startTs = data.getLong("startTs");
|
||||
@@ -62,7 +67,7 @@ public class JcppTransactionConsumer {
|
||||
JSONObject detail = data.getJSONObject("detail");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.warn("交易记录消息缺少 tradeNo: {}", message);
|
||||
log.warn("交易记录消息缺少 tradeNo");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +127,8 @@ public class JcppTransactionConsumer {
|
||||
// orderBasicInfoService.realTimeOrderSplit(order.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 JCPP 交易记录消息异常: message={}", message, e);
|
||||
log.error("处理 JCPP 交易记录消息异常: pileCode={}",
|
||||
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class JcppUplinkMessage implements Serializable {
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 具体消息内容(根据 messageType 不同,结构不同)
|
||||
* 具体消息内容(JSON 字符串格式,根据 messageType 不同,结构不同)
|
||||
*/
|
||||
private Object data;
|
||||
private String data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jsowell.pile.jcpp.dto.sync;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* JCPP 充电枪同步数据传输对象
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
public class JcppGunSyncDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 充电枪编码(对应 Web 的 pile_connector_code)
|
||||
*/
|
||||
private String gunCode;
|
||||
|
||||
/**
|
||||
* 充电枪名称(对应 Web 的 name)
|
||||
*/
|
||||
private String gunName;
|
||||
|
||||
/**
|
||||
* 枪号(从 gunCode 提取最后 2 位)
|
||||
*/
|
||||
private String gunNo;
|
||||
|
||||
/**
|
||||
* 所属充电桩编码(对应 Web 的 pile_sn)
|
||||
*/
|
||||
private String pileCode;
|
||||
|
||||
/**
|
||||
* 附加信息(JSON 格式)
|
||||
* 包含:webGunId, status, parkNo 等
|
||||
*/
|
||||
private JSONObject additionalInfo;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.jsowell.pile.jcpp.dto.sync;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步数据传输对象
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
public class JcppPileSyncDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 充电桩编码(对应 Web 的 sn)
|
||||
*/
|
||||
private String pileCode;
|
||||
|
||||
/**
|
||||
* 充电桩名称(对应 Web 的 name)
|
||||
*/
|
||||
private String pileName;
|
||||
|
||||
/**
|
||||
* 软件协议(对应 Web 的 software_protocol)
|
||||
*/
|
||||
private String protocol;
|
||||
|
||||
/**
|
||||
* 品牌
|
||||
*/
|
||||
private String brand;
|
||||
|
||||
/**
|
||||
* 型号
|
||||
*/
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 制造商
|
||||
*/
|
||||
private String manufacturer;
|
||||
|
||||
/**
|
||||
* 类型:OPERATION-运营桩, PERSONAL-个人桩
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 附加信息(JSON 格式)
|
||||
* 包含:webPileId, webStationId, businessType, secretKey, longitude, latitude, iccid 等
|
||||
*/
|
||||
private JSONObject additionalInfo;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jsowell.pile.jcpp.dto.sync;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP 同步请求
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
public class JcppSyncRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 同步类型:FULL-全量, INCREMENTAL-增量
|
||||
*/
|
||||
private String syncType;
|
||||
|
||||
/**
|
||||
* 上次同步时间(增量同步时使用)
|
||||
*/
|
||||
private Date lastSyncTime;
|
||||
|
||||
/**
|
||||
* 充电桩列表
|
||||
*/
|
||||
private List<JcppPileSyncDTO> piles;
|
||||
|
||||
/**
|
||||
* 充电枪列表
|
||||
*/
|
||||
private List<JcppGunSyncDTO> guns;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.jsowell.pile.jcpp.dto.sync;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP 同步响应
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
public class JcppSyncResponse implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 总充电桩数
|
||||
*/
|
||||
private Integer totalPiles;
|
||||
|
||||
/**
|
||||
* 成功充电桩数
|
||||
*/
|
||||
private Integer successPiles;
|
||||
|
||||
/**
|
||||
* 失败充电桩数
|
||||
*/
|
||||
private Integer failedPiles;
|
||||
|
||||
/**
|
||||
* 总充电枪数
|
||||
*/
|
||||
private Integer totalGuns;
|
||||
|
||||
/**
|
||||
* 成功充电枪数
|
||||
*/
|
||||
private Integer successGuns;
|
||||
|
||||
/**
|
||||
* 失败充电枪数
|
||||
*/
|
||||
private Integer failedGuns;
|
||||
|
||||
/**
|
||||
* 同步结果列表
|
||||
*/
|
||||
private List<JcppSyncResult> results;
|
||||
|
||||
/**
|
||||
* 错误信息列表
|
||||
*/
|
||||
private List<String> errors;
|
||||
|
||||
/**
|
||||
* 构建响应
|
||||
*/
|
||||
public static JcppSyncResponse build(List<JcppSyncResult> pileResults, List<JcppSyncResult> gunResults) {
|
||||
JcppSyncResponse response = new JcppSyncResponse();
|
||||
|
||||
// 统计充电桩结果
|
||||
int totalPiles = pileResults != null ? pileResults.size() : 0;
|
||||
int successPiles = 0;
|
||||
int failedPiles = 0;
|
||||
|
||||
if (pileResults != null) {
|
||||
for (JcppSyncResult result : pileResults) {
|
||||
if (Boolean.TRUE.equals(result.getSuccess())) {
|
||||
successPiles++;
|
||||
} else {
|
||||
failedPiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 统计充电枪结果
|
||||
int totalGuns = gunResults != null ? gunResults.size() : 0;
|
||||
int successGuns = 0;
|
||||
int failedGuns = 0;
|
||||
|
||||
if (gunResults != null) {
|
||||
for (JcppSyncResult result : gunResults) {
|
||||
if (Boolean.TRUE.equals(result.getSuccess())) {
|
||||
successGuns++;
|
||||
} else {
|
||||
failedGuns++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置统计信息
|
||||
response.setTotalPiles(totalPiles);
|
||||
response.setSuccessPiles(successPiles);
|
||||
response.setFailedPiles(failedPiles);
|
||||
response.setTotalGuns(totalGuns);
|
||||
response.setSuccessGuns(successGuns);
|
||||
response.setFailedGuns(failedGuns);
|
||||
|
||||
// 合并结果
|
||||
List<JcppSyncResult> allResults = new ArrayList<>();
|
||||
if (pileResults != null) {
|
||||
allResults.addAll(pileResults);
|
||||
}
|
||||
if (gunResults != null) {
|
||||
allResults.addAll(gunResults);
|
||||
}
|
||||
response.setResults(allResults);
|
||||
|
||||
// 收集错误信息
|
||||
List<String> errors = new ArrayList<>();
|
||||
for (JcppSyncResult result : allResults) {
|
||||
if (Boolean.FALSE.equals(result.getSuccess())) {
|
||||
errors.add(result.getCode() + ": " + result.getMessage());
|
||||
}
|
||||
}
|
||||
response.setErrors(errors);
|
||||
|
||||
// 判断整体是否成功
|
||||
boolean overallSuccess = (failedPiles == 0 && failedGuns == 0);
|
||||
response.setSuccess(overallSuccess);
|
||||
response.setMessage(overallSuccess ? "同步成功" : "同步部分失败");
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.jsowell.pile.jcpp.dto.sync;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* JCPP 单个同步结果
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Data
|
||||
public class JcppSyncResult implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 充电桩编码或充电枪编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* JCPP 返回的 ID(UUID)
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*/
|
||||
public static JcppSyncResult success(String code, String id, String message) {
|
||||
JcppSyncResult result = new JcppSyncResult();
|
||||
result.setCode(code);
|
||||
result.setId(id);
|
||||
result.setSuccess(true);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败结果
|
||||
*/
|
||||
public static JcppSyncResult fail(String code, String message) {
|
||||
JcppSyncResult result = new JcppSyncResult();
|
||||
result.setCode(code);
|
||||
result.setSuccess(false);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.jsowell.pile.jcpp.service;
|
||||
|
||||
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
|
||||
|
||||
/**
|
||||
* JCPP JSON 消息处理器接口
|
||||
* 处理从 JCPP 接收到的各种上行消息(JSON 格式)
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public interface IJcppJsonMessageHandler {
|
||||
|
||||
/**
|
||||
* 处理上行消息
|
||||
*
|
||||
* @param message JSON 上行消息
|
||||
*/
|
||||
void handleUplinkMessage(JcppUplinkMessage message);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.jsowell.pile.jcpp.service;
|
||||
|
||||
import com.jsowell.pile.jcpp.dto.sync.JcppSyncResponse;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步服务接口
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public interface IJcppPileSyncService {
|
||||
|
||||
/**
|
||||
* 全量同步充电桩数据到 JCPP
|
||||
*
|
||||
* @return 同步结果
|
||||
*/
|
||||
JcppSyncResponse syncAllPiles();
|
||||
|
||||
/**
|
||||
* 增量同步充电桩数据到 JCPP
|
||||
*
|
||||
* @param lastSyncTime 上次同步时间(可选,如果为 null 则查询最后一次成功的同步记录)
|
||||
* @return 同步结果
|
||||
*/
|
||||
JcppSyncResponse syncIncrementalPiles(Date lastSyncTime);
|
||||
|
||||
/**
|
||||
* 同步单个充电桩
|
||||
*
|
||||
* @param pileSn 充电桩编号
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean syncSinglePile(String pileSn);
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
package com.jsowell.pile.jcpp.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.jsowell.common.enums.ykc.StartTypeEnum;
|
||||
import com.jsowell.common.util.StringUtils;
|
||||
import com.jsowell.common.util.id.IdUtils;
|
||||
import com.jsowell.pile.domain.*;
|
||||
import com.jsowell.pile.jcpp.constant.JcppConstants;
|
||||
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
|
||||
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
|
||||
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
|
||||
import com.jsowell.pile.jcpp.service.IJcppJsonMessageHandler;
|
||||
import com.jsowell.pile.jcpp.util.PricingModelConverter;
|
||||
import com.jsowell.pile.service.*;
|
||||
import com.jsowell.pile.vo.web.BillingTemplateVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JCPP JSON 消息处理器实现
|
||||
* 整合原有各个消费者的处理逻辑,支持分区消费
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JcppJsonMessageHandlerImpl implements IJcppJsonMessageHandler {
|
||||
|
||||
private static final String HEARTBEAT_KEY_PREFIX = "jcpp:heartbeat:";
|
||||
private static final long HEARTBEAT_EXPIRE_SECONDS = 180L;
|
||||
private static final String REALTIME_DATA_KEY_PREFIX = "jcpp:realtime:";
|
||||
private static final long REALTIME_DATA_EXPIRE_SECONDS = 300L;
|
||||
|
||||
@Autowired
|
||||
private PileBasicInfoService pileBasicInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileConnectorInfoService pileConnectorInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileAuthCardService pileAuthCardService;
|
||||
|
||||
@Autowired
|
||||
private MemberBasicInfoService memberBasicInfoService;
|
||||
|
||||
@Autowired
|
||||
private MemberWalletInfoService memberWalletInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileStationWhitelistService pileStationWhitelistService;
|
||||
|
||||
@Autowired
|
||||
private OrderBasicInfoService orderBasicInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileBillingTemplateService pileBillingTemplateService;
|
||||
|
||||
@Autowired
|
||||
private IJcppDownlinkService jcppDownlinkService;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
public void handleUplinkMessage(JcppUplinkMessage uplinkMessage) {
|
||||
String messageType = uplinkMessage.getMessageType();
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
|
||||
log.debug("处理上行消息: pileCode={}, messageType={}", pileCode, messageType);
|
||||
|
||||
// 根据消息类型分发处理
|
||||
switch (messageType) {
|
||||
case JcppConstants.MessageType.LOGIN:
|
||||
handleLogin(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.HEARTBEAT:
|
||||
handleHeartbeat(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.GUN_STATUS:
|
||||
handleGunStatus(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.REAL_TIME_DATA:
|
||||
handleRealTimeData(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.TRANSACTION_RECORD:
|
||||
handleTransactionRecord(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.START_CHARGE:
|
||||
handleStartCharge(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.QUERY_PRICING:
|
||||
handleQueryPricing(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.VERIFY_PRICING:
|
||||
handleVerifyPricing(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.SESSION_CLOSE:
|
||||
handleSessionClose(uplinkMessage);
|
||||
break;
|
||||
case JcppConstants.MessageType.REMOTE_START_RESULT:
|
||||
case JcppConstants.MessageType.REMOTE_STOP_RESULT:
|
||||
handleRemoteResult(uplinkMessage);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的消息类型: messageType={}, pileCode={}", messageType, pileCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录请求
|
||||
*/
|
||||
private void handleLogin(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
|
||||
log.info("处理登录请求: pileCode={}", pileCode);
|
||||
|
||||
// 查询充电桩是否存在
|
||||
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
|
||||
boolean exists = pileInfo != null;
|
||||
|
||||
if (exists) {
|
||||
log.info("充电桩登录成功: pileCode={}", pileCode);
|
||||
} else {
|
||||
log.warn("充电桩不存在: pileCode={}", pileCode);
|
||||
}
|
||||
|
||||
// 发送登录应答
|
||||
jcppDownlinkService.sendLoginAck(pileCode, exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳请求
|
||||
*/
|
||||
private void handleHeartbeat(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
|
||||
// 更新最后活跃时间到 Redis
|
||||
String key = HEARTBEAT_KEY_PREFIX + pileCode;
|
||||
stringRedisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),
|
||||
HEARTBEAT_EXPIRE_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("收到充电桩心跳: pileCode={}", pileCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理枪状态上报
|
||||
*/
|
||||
private void handleGunStatus(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String gunRunStatus = data.getString("gunRunStatus");
|
||||
|
||||
log.info("处理枪状态: pileCode={}, gunNo={}, status={}", pileCode, gunNo, gunRunStatus);
|
||||
|
||||
// 映射状态
|
||||
String systemStatus = mapGunStatus(gunRunStatus);
|
||||
|
||||
// 更新枪状态
|
||||
String pileConnectorCode = pileCode + gunNo;
|
||||
int result = pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, systemStatus);
|
||||
|
||||
if (result > 0) {
|
||||
log.info("更新枪状态成功: pileConnectorCode={}, status={}", pileConnectorCode, systemStatus);
|
||||
} else {
|
||||
log.warn("更新枪状态失败: pileConnectorCode={}", pileConnectorCode);
|
||||
}
|
||||
|
||||
// 记录故障信息
|
||||
if (data.containsKey("faultMessages") && data.getJSONArray("faultMessages") != null) {
|
||||
log.warn("充电枪故障: pileConnectorCode={}, faults={}", pileConnectorCode, data.getJSONArray("faultMessages"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理充电进度数据
|
||||
*/
|
||||
private void handleRealTimeData(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.debug("实时数据消息缺少 tradeNo");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将实时数据缓存到 Redis
|
||||
String key = REALTIME_DATA_KEY_PREFIX + tradeNo;
|
||||
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(data),
|
||||
REALTIME_DATA_EXPIRE_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
// 根据 tradeNo 查询订单
|
||||
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
|
||||
if (order != null && "1".equals(order.getOrderStatus())) {
|
||||
// 更新订单实时数据
|
||||
OrderBasicInfo updateOrder = new OrderBasicInfo();
|
||||
updateOrder.setId(order.getId());
|
||||
String totalChargingCostYuan = data.getString("totalChargingCostYuan");
|
||||
if (totalChargingCostYuan != null && !totalChargingCostYuan.isEmpty()) {
|
||||
updateOrder.setOrderAmount(new BigDecimal(totalChargingCostYuan));
|
||||
}
|
||||
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
|
||||
}
|
||||
|
||||
// 更新枪状态为充电中
|
||||
String pileConnectorCode = pileCode + gunNo;
|
||||
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
|
||||
|
||||
log.debug("处理充电进度: tradeNo={}, pileCode={}", tradeNo, pileCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理交易记录
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
private void handleTransactionRecord(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.warn("交易记录消息缺少 tradeNo");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("处理交易记录: tradeNo={}, pileCode={}, gunNo={}", tradeNo, pileCode, gunNo);
|
||||
|
||||
// 根据 tradeNo 查询订单
|
||||
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
|
||||
if (order == null) {
|
||||
log.warn("订单不存在: tradeNo={}", tradeNo);
|
||||
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等性检查
|
||||
if ("2".equals(order.getOrderStatus())) {
|
||||
log.info("订单已处理,跳过: tradeNo={}", tradeNo);
|
||||
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新订单信息
|
||||
OrderBasicInfo updateOrder = new OrderBasicInfo();
|
||||
updateOrder.setId(order.getId());
|
||||
updateOrder.setOrderStatus("2"); // 充电完成
|
||||
|
||||
Long startTs = data.getLong("startTs");
|
||||
Long endTs = data.getLong("endTs");
|
||||
if (startTs != null && startTs > 0) {
|
||||
updateOrder.setChargeStartTime(new Date(startTs));
|
||||
}
|
||||
if (endTs != null && endTs > 0) {
|
||||
updateOrder.setChargeEndTime(new Date(endTs));
|
||||
}
|
||||
|
||||
String totalAmountYuan = data.getString("totalAmountYuan");
|
||||
if (totalAmountYuan != null && !totalAmountYuan.isEmpty()) {
|
||||
updateOrder.setOrderAmount(new BigDecimal(totalAmountYuan));
|
||||
}
|
||||
|
||||
String stopReason = data.getString("stopReason");
|
||||
if (stopReason != null && !stopReason.isEmpty()) {
|
||||
updateOrder.setReason(stopReason);
|
||||
}
|
||||
|
||||
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
|
||||
log.info("更新订单完成: tradeNo={}", tradeNo);
|
||||
|
||||
// 更新枪状态为空闲
|
||||
String pileConnectorCode = pileCode + gunNo;
|
||||
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "1");
|
||||
|
||||
// 发送交易记录应答
|
||||
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
|
||||
|
||||
// TODO: 触发结算流程
|
||||
// orderBasicInfoService.realTimeOrderSplit(order.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理启动充电请求(刷卡)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
private void handleStartCharge(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String gunNo = data.getString("gunNo");
|
||||
String startType = data.getString("startType");
|
||||
String cardNo = data.getString("cardNo");
|
||||
|
||||
if (pileCode == null || gunNo == null) {
|
||||
log.warn("启动充电消息缺少必要字段");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("处理启动充电请求: pileCode={}, gunNo={}, cardNo={}", pileCode, gunNo, cardNo);
|
||||
|
||||
// 处理刷卡启动
|
||||
if ("CARD".equals(startType)) {
|
||||
handleCardStartCharge(pileCode, gunNo, cardNo);
|
||||
} else {
|
||||
log.warn("不支持的启动类型: {}", startType);
|
||||
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, null, cardNo, null,
|
||||
false, "不支持的启动类型");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理刷卡启动充电
|
||||
*/
|
||||
private void handleCardStartCharge(String pileCode, String gunNo, String cardNo) {
|
||||
String failReason = null;
|
||||
String tradeNo = null;
|
||||
String limitYuan = null;
|
||||
boolean authSuccess = false;
|
||||
|
||||
try {
|
||||
// 1. 查询充电桩信息
|
||||
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
|
||||
if (pileInfo == null) {
|
||||
failReason = "充电桩不存在";
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查充电桩状态
|
||||
if (pileInfo.getDelFlag() != null && StringUtils.equals(pileInfo.getDelFlag(), "1")) {
|
||||
failReason = "充电桩已停用";
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 查询授权卡信息
|
||||
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
|
||||
if (authCard == null) {
|
||||
failReason = "账户不存在";
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查卡状态
|
||||
if (authCard.getStatus() != null && !StringUtils.equals(authCard.getStatus(), "1")) {
|
||||
failReason = "账户已冻结";
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 查询会员信息和钱包余额
|
||||
String memberId = authCard.getMemberId();
|
||||
BigDecimal balance = BigDecimal.ZERO;
|
||||
|
||||
if (memberId != null) {
|
||||
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, String.valueOf(pileInfo.getMerchantId()));
|
||||
if (walletInfo != null) {
|
||||
balance = walletInfo.getPrincipalBalance();
|
||||
if (walletInfo.getGiftBalance() != null) {
|
||||
balance = balance.add(walletInfo.getGiftBalance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查白名单
|
||||
boolean isWhitelist = checkWhitelist(pileInfo.getStationId(), cardNo, memberId);
|
||||
|
||||
// 5. 验证余额(非白名单用户需要检查余额)
|
||||
BigDecimal minAmount = new BigDecimal("1.00");
|
||||
if (!isWhitelist && balance.compareTo(minAmount) < 0) {
|
||||
failReason = "余额不足";
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 生成交易流水号
|
||||
tradeNo = IdUtils.fastSimpleUUID();
|
||||
limitYuan = balance.toString();
|
||||
|
||||
// 7. 创建充电订单
|
||||
OrderBasicInfo order = new OrderBasicInfo();
|
||||
order.setOrderCode(tradeNo);
|
||||
order.setPileSn(pileCode);
|
||||
order.setConnectorCode(pileCode + gunNo);
|
||||
order.setMemberId(memberId);
|
||||
order.setStationId(String.valueOf(pileInfo.getStationId()));
|
||||
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
|
||||
order.setOrderStatus("0"); // 待支付/启动中
|
||||
order.setPayMode(String.valueOf(isWhitelist ? 3 : 1)); // 3-白名单支付, 1-余额支付
|
||||
order.setCreateTime(new Date());
|
||||
order.setStartType(StartTypeEnum.NOW.getValue());
|
||||
|
||||
orderBasicInfoService.insert(order);
|
||||
log.info("创建充电订单: tradeNo={}, pileCode={}, gunNo={}", tradeNo, pileCode, gunNo);
|
||||
|
||||
authSuccess = true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理刷卡启动充电异常: pileCode={}, gunNo={}, cardNo={}", pileCode, gunNo, cardNo, e);
|
||||
failReason = "系统错误";
|
||||
}
|
||||
|
||||
// 发送鉴权结果
|
||||
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, authSuccess, failReason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查白名单
|
||||
*/
|
||||
private boolean checkWhitelist(Long stationId, String cardNo, String memberId) {
|
||||
try {
|
||||
if (stationId == null) {
|
||||
return false;
|
||||
}
|
||||
PileStationWhitelist pileStationWhitelist = pileStationWhitelistService.queryWhitelistByMemberId(String.valueOf(stationId), memberId);
|
||||
return pileStationWhitelist != null;
|
||||
} catch (Exception e) {
|
||||
log.error("检查白名单异常: stationId={}", stationId, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送启动充电应答
|
||||
*/
|
||||
private void sendStartChargeAck(String pileCode, String gunNo, String tradeNo, String cardNo,
|
||||
String limitYuan, boolean authSuccess, String failReason) {
|
||||
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan,
|
||||
authSuccess, failReason);
|
||||
if (authSuccess) {
|
||||
log.info("刷卡鉴权成功: pileCode={}, gunNo={}, cardNo={}, tradeNo={}",
|
||||
pileCode, gunNo, cardNo, tradeNo);
|
||||
} else {
|
||||
log.warn("刷卡鉴权失败: pileCode={}, gunNo={}, cardNo={}, reason={}",
|
||||
pileCode, gunNo, cardNo, failReason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询计费模板请求
|
||||
*/
|
||||
private void handleQueryPricing(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
|
||||
log.info("处理查询计费模板请求: pileCode={}", pileCode);
|
||||
|
||||
try {
|
||||
// 根据 pileCode 查询充电桩
|
||||
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
|
||||
if (pileInfo == null) {
|
||||
log.warn("充电桩不存在: pileCode={}", pileCode);
|
||||
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取关联的计费模板
|
||||
BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
|
||||
if (billingTemplateVO == null || billingTemplateVO.getTemplateId() == null) {
|
||||
log.warn("充电桩未配置计费模板: pileCode={}", pileCode);
|
||||
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
|
||||
return;
|
||||
}
|
||||
Long billingTemplateId = Long.parseLong(billingTemplateVO.getTemplateId());
|
||||
|
||||
// 查询计费模板
|
||||
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(billingTemplateId);
|
||||
if (template == null) {
|
||||
log.warn("计费模板不存在: pileCode={}, billingTemplateId={}", pileCode, billingTemplateId);
|
||||
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换为 JCPP 格式
|
||||
JcppPricingModel pricingModel = PricingModelConverter.convert(template);
|
||||
|
||||
// 发送应答
|
||||
jcppDownlinkService.sendQueryPricingAck(pileCode, billingTemplateId, pricingModel);
|
||||
log.info("发送计费模板查询应答: pileCode={}, pricingId={}", pileCode, billingTemplateId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理查询计费模板异常: pileCode=", pileCode, e);
|
||||
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理校验计费模板请求
|
||||
*/
|
||||
private void handleVerifyPricing(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
Long pricingId = data.getLong("pricingId");
|
||||
|
||||
log.info("处理校验计费模板请求: pileCode={}, pricingId={}", pileCode, pricingId);
|
||||
|
||||
try {
|
||||
boolean success = false;
|
||||
|
||||
if (pricingId != null) {
|
||||
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(pricingId);
|
||||
success = template != null;
|
||||
}
|
||||
|
||||
// 发送应答
|
||||
jcppDownlinkService.sendVerifyPricingAck(pileCode, success, pricingId);
|
||||
log.info("发送计费模板校验应答: pileCode={}, pricingId={}, success={}", pileCode, pricingId, success);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理校验计费模板异常: pileCode={}, pricingId={}", pileCode, pricingId, e);
|
||||
jcppDownlinkService.sendVerifyPricingAck(pileCode, false, pricingId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理会话关闭事件
|
||||
*/
|
||||
private void handleSessionClose(JcppUplinkMessage uplinkMessage) {
|
||||
String pileCode = uplinkMessage.getPileCode();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
String reason = data.getString("reason");
|
||||
|
||||
log.info("处理会话关闭: pileCode={}, reason={}", pileCode, reason);
|
||||
|
||||
// 更新所有枪状态为离线
|
||||
int result = pileConnectorInfoService.updateConnectorStatusByPileSn(pileCode, "0");
|
||||
log.info("更新枪状态为离线: pileCode={}, affectedRows={}", pileCode, result);
|
||||
|
||||
// TODO: 查询是否有正在充电的订单,如果有则标记为异常
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理远程操作结果
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
private void handleRemoteResult(JcppUplinkMessage uplinkMessage) {
|
||||
String messageType = uplinkMessage.getMessageType();
|
||||
JSONObject data = JSON.parseObject(uplinkMessage.getData());
|
||||
|
||||
if (JcppConstants.MessageType.REMOTE_START_RESULT.equals(messageType)) {
|
||||
handleRemoteStartResult(data);
|
||||
} else if (JcppConstants.MessageType.REMOTE_STOP_RESULT.equals(messageType)) {
|
||||
handleRemoteStopResult(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理远程启动结果
|
||||
*/
|
||||
private void handleRemoteStartResult(JSONObject data) {
|
||||
String tradeNo = data.getString("tradeNo");
|
||||
Boolean success = data.getBoolean("success");
|
||||
String failReason = data.getString("failReason");
|
||||
|
||||
// 从 data 中获取 pileCode 和 gunNo(如果有)
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
|
||||
if (tradeNo == null || tradeNo.isEmpty()) {
|
||||
log.warn("远程启动结果缺少 tradeNo");
|
||||
return;
|
||||
}
|
||||
|
||||
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
|
||||
if (order == null) {
|
||||
log.warn("订单不存在: tradeNo={}", tradeNo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
// 启动成功
|
||||
OrderBasicInfo updateOrder = new OrderBasicInfo();
|
||||
updateOrder.setId(order.getId());
|
||||
updateOrder.setOrderStatus("1"); // 充电中
|
||||
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
|
||||
|
||||
String pileConnectorCode = pileCode + gunNo;
|
||||
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
|
||||
|
||||
log.info("远程启动成功: tradeNo={}", tradeNo);
|
||||
} else {
|
||||
// 启动失败
|
||||
OrderBasicInfo updateOrder = new OrderBasicInfo();
|
||||
updateOrder.setId(order.getId());
|
||||
updateOrder.setOrderStatus("3"); // 已取消
|
||||
updateOrder.setReason("启动失败: " + failReason);
|
||||
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
|
||||
|
||||
log.warn("远程启动失败: tradeNo={}, reason={}", tradeNo, failReason);
|
||||
|
||||
// TODO: 如果已预付费,触发退款流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理远程停止结果
|
||||
*/
|
||||
private void handleRemoteStopResult(JSONObject data) {
|
||||
Boolean success = data.getBoolean("success");
|
||||
String failReason = data.getString("failReason");
|
||||
|
||||
// 从 data 中获取 pileCode 和 gunNo(如果有)
|
||||
String pileCode = data.getString("pileCode");
|
||||
String gunNo = data.getString("gunNo");
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
log.info("远程停止成功: pileCode={}, gunNo={}", pileCode, gunNo);
|
||||
} else {
|
||||
log.warn("远程停止失败: pileCode=, gunNo={}, reason={}", pileCode, gunNo, failReason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射枪状态:JCPP 状态 -> 系统状态
|
||||
*/
|
||||
private String mapGunStatus(String jcppStatus) {
|
||||
if (jcppStatus == null) {
|
||||
return "0";
|
||||
}
|
||||
switch (jcppStatus) {
|
||||
case "IDLE":
|
||||
return "1"; // 空闲
|
||||
case "INSERTED":
|
||||
return "2"; // 占用(未充电)
|
||||
case "CHARGING":
|
||||
return "3"; // 占用(充电中)
|
||||
case "CHARGE_COMPLETE":
|
||||
return "2"; // 占用(未充电)- 充电完成但未拔枪
|
||||
case "FAULT":
|
||||
return "255"; // 故障
|
||||
case "UNKNOWN":
|
||||
default:
|
||||
return "0"; // 离网
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package com.jsowell.pile.jcpp.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.jsowell.common.util.StringUtils;
|
||||
import com.jsowell.pile.domain.JcppSyncRecord;
|
||||
import com.jsowell.pile.domain.PileBasicInfo;
|
||||
import com.jsowell.pile.domain.PileConnectorInfo;
|
||||
import com.jsowell.pile.jcpp.dto.sync.*;
|
||||
import com.jsowell.pile.jcpp.service.IJcppPileSyncService;
|
||||
import com.jsowell.pile.mapper.JcppSyncRecordMapper;
|
||||
import com.jsowell.pile.service.PileBasicInfoService;
|
||||
import com.jsowell.pile.service.PileConnectorInfoService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步服务实现
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
|
||||
|
||||
@Autowired
|
||||
private PileBasicInfoService pileBasicInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileConnectorInfoService pileConnectorInfoService;
|
||||
|
||||
@Autowired
|
||||
private JcppSyncRecordMapper jcppSyncRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Value("${jcpp.sync.api-url:http://localhost:8080/api/sync}")
|
||||
private String jcppApiUrl;
|
||||
|
||||
@Value("${jcpp.sync.batch-size:100}")
|
||||
private int batchSize;
|
||||
|
||||
@Value("${jcpp.sync.timeout:60000}")
|
||||
private int timeout;
|
||||
|
||||
/**
|
||||
* 全量同步充电桩数据到 JCPP
|
||||
*/
|
||||
@Override
|
||||
public JcppSyncResponse syncAllPiles() {
|
||||
log.info("开始全量同步充电桩数据到 JCPP");
|
||||
|
||||
// 创建同步记录
|
||||
JcppSyncRecord record = createSyncRecord("FULL");
|
||||
|
||||
try {
|
||||
// 1. 查询所有充电桩(未删除的)
|
||||
PileBasicInfo queryPile = new PileBasicInfo();
|
||||
queryPile.setDelFlag("0");
|
||||
List<PileBasicInfo> pileList = pileBasicInfoService.selectPileBasicInfoList(queryPile);
|
||||
|
||||
log.info("查询到 {} 个充电桩", pileList.size());
|
||||
|
||||
// 2. 查询所有充电枪(未删除的)
|
||||
PileConnectorInfo queryGun = new PileConnectorInfo();
|
||||
queryGun.setDelFlag("0");
|
||||
List<PileConnectorInfo> gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
|
||||
|
||||
log.info("查询到 {} 个充电枪", gunList.size());
|
||||
|
||||
// 3. 转换数据格式
|
||||
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(pileList);
|
||||
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
|
||||
|
||||
// 4. 调用 JCPP 同步接口
|
||||
JcppSyncResponse response = callJcppSyncApi(pileDTOs, gunDTOs);
|
||||
|
||||
// 5. 更新同步记录
|
||||
updateSyncRecord(record, response, "SUCCESS");
|
||||
|
||||
log.info("全量同步完成: 充电桩 {}/{}, 充电枪 {}/{}",
|
||||
response.getSuccessPiles(), response.getTotalPiles(),
|
||||
response.getSuccessGuns(), response.getTotalGuns());
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("全量同步失败", e);
|
||||
updateSyncRecord(record, null, "FAILED", e.getMessage());
|
||||
throw new RuntimeException("全量同步失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量同步充电桩数据到 JCPP
|
||||
*/
|
||||
@Override
|
||||
public JcppSyncResponse syncIncrementalPiles(Date lastSyncTime) {
|
||||
log.info("开始增量同步充电桩数据到 JCPP");
|
||||
|
||||
// 如果未指定上次同步时间,查询最后一次成功的同步记录
|
||||
if (lastSyncTime == null) {
|
||||
JcppSyncRecord lastRecord = jcppSyncRecordMapper.selectLastSuccessRecord("INCREMENTAL");
|
||||
if (lastRecord != null) {
|
||||
lastSyncTime = lastRecord.getStartTime();
|
||||
log.info("使用最后一次成功同步时间: {}", lastSyncTime);
|
||||
} else {
|
||||
// 如果没有历史记录,使用全量同步
|
||||
log.warn("未找到历史同步记录,改为全量同步");
|
||||
return syncAllPiles();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建同步记录
|
||||
JcppSyncRecord record = createSyncRecord("INCREMENTAL");
|
||||
|
||||
try {
|
||||
// 1. 查询更新时间大于 lastSyncTime 的充电桩
|
||||
// 注意:这里暂时使用全量查询,然后在内存中过滤
|
||||
// TODO: 后续可以在 Mapper 中添加按 updateTime 查询的方法以提升性能
|
||||
PileBasicInfo queryPile = new PileBasicInfo();
|
||||
queryPile.setDelFlag("0");
|
||||
List<PileBasicInfo> allPiles = pileBasicInfoService.selectPileBasicInfoList(queryPile);
|
||||
|
||||
// 过滤出更新时间大于 lastSyncTime 的充电桩
|
||||
final Date finalLastSyncTime = lastSyncTime;
|
||||
List<PileBasicInfo> pileList = allPiles.stream()
|
||||
.filter(pile -> pile.getUpdateTime() != null && pile.getUpdateTime().after(finalLastSyncTime))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
log.info("查询到 {} 个更新的充电桩", pileList.size());
|
||||
|
||||
// 2. 查询更新时间大于 lastSyncTime 的充电枪
|
||||
PileConnectorInfo queryGun = new PileConnectorInfo();
|
||||
queryGun.setDelFlag("0");
|
||||
List<PileConnectorInfo> allGuns = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
|
||||
|
||||
// 过滤出更新时间大于 lastSyncTime 的充电枪
|
||||
List<PileConnectorInfo> gunList = allGuns.stream()
|
||||
.filter(gun -> gun.getUpdateTime() != null && gun.getUpdateTime().after(finalLastSyncTime))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
log.info("查询到 {} 个更新的充电枪", gunList.size());
|
||||
|
||||
// 3. 转换数据格式
|
||||
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(pileList);
|
||||
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
|
||||
|
||||
// 4. 调用 JCPP 同步接口
|
||||
JcppSyncResponse response = callJcppSyncApi(pileDTOs, gunDTOs);
|
||||
|
||||
// 5. 更新同步记录
|
||||
updateSyncRecord(record, response, "SUCCESS");
|
||||
|
||||
log.info("增量同步完成: 充电桩 {}/{}, 充电枪 {}/{}",
|
||||
response.getSuccessPiles(), response.getTotalPiles(),
|
||||
response.getSuccessGuns(), response.getTotalGuns());
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("增量同步失败", e);
|
||||
updateSyncRecord(record, null, "FAILED", e.getMessage());
|
||||
throw new RuntimeException("增量同步失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步单个充电桩
|
||||
*/
|
||||
@Override
|
||||
public boolean syncSinglePile(String pileSn) {
|
||||
log.info("开始同步单个充电桩: {}", pileSn);
|
||||
|
||||
try {
|
||||
// 1. 查询充电桩
|
||||
PileBasicInfo pile = pileBasicInfoService.selectPileBasicInfoBySN(pileSn);
|
||||
if (pile == null) {
|
||||
log.warn("充电桩不存在: {}", pileSn);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 查询该充电桩的所有充电枪
|
||||
PileConnectorInfo queryGun = new PileConnectorInfo();
|
||||
queryGun.setPileSn(pileSn);
|
||||
queryGun.setDelFlag("0");
|
||||
List<PileConnectorInfo> gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
|
||||
|
||||
// 3. 转换数据格式
|
||||
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(List.of(pile));
|
||||
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
|
||||
|
||||
// 4. 调用 JCPP 同步接口
|
||||
JcppSyncResponse response = callJcppSyncApi(pileDTOs, gunDTOs);
|
||||
|
||||
log.info("单个充电桩同步完成: {}, 结果: {}", pileSn, response.getSuccess());
|
||||
|
||||
return response.getSuccess();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步单个充电桩失败: {}", pileSn, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换充电桩数据为 DTO
|
||||
*/
|
||||
private List<JcppPileSyncDTO> convertPilesToDTO(List<PileBasicInfo> pileList) {
|
||||
List<JcppPileSyncDTO> dtoList = new ArrayList<>();
|
||||
|
||||
for (PileBasicInfo pile : pileList) {
|
||||
JcppPileSyncDTO dto = new JcppPileSyncDTO();
|
||||
|
||||
// 基本字段
|
||||
dto.setPileCode(pile.getSn());
|
||||
dto.setPileName(pile.getName());
|
||||
dto.setProtocol(pile.getSoftwareProtocol());
|
||||
|
||||
// 品牌、型号、制造商(可为空)
|
||||
dto.setBrand(null); // Web 项目中没有这些字段
|
||||
dto.setModel(null);
|
||||
dto.setManufacturer(null);
|
||||
|
||||
// 类型映射:1-运营桩 → OPERATION, 2-个人桩 → PERSONAL
|
||||
String type = "OPERATION"; // 默认运营桩
|
||||
if ("2".equals(pile.getBusinessType())) {
|
||||
type = "PERSONAL";
|
||||
}
|
||||
dto.setType(type);
|
||||
|
||||
// 构建附加信息
|
||||
JSONObject additionalInfo = new JSONObject();
|
||||
additionalInfo.put("webPileId", pile.getId());
|
||||
additionalInfo.put("webStationId", pile.getStationId());
|
||||
additionalInfo.put("businessType", pile.getBusinessType());
|
||||
additionalInfo.put("secretKey", pile.getSecretKey());
|
||||
additionalInfo.put("longitude", pile.getLongitude());
|
||||
additionalInfo.put("latitude", pile.getLatitude());
|
||||
additionalInfo.put("iccid", pile.getIccid());
|
||||
additionalInfo.put("merchantId", pile.getMerchantId());
|
||||
additionalInfo.put("vinFlag", pile.getVinFlag());
|
||||
|
||||
dto.setAdditionalInfo(additionalInfo);
|
||||
|
||||
dtoList.add(dto);
|
||||
}
|
||||
|
||||
return dtoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换充电枪数据为 DTO
|
||||
*/
|
||||
private List<JcppGunSyncDTO> convertGunsToDTO(List<PileConnectorInfo> gunList) {
|
||||
List<JcppGunSyncDTO> dtoList = new ArrayList<>();
|
||||
|
||||
for (PileConnectorInfo gun : gunList) {
|
||||
JcppGunSyncDTO dto = new JcppGunSyncDTO();
|
||||
|
||||
// 基本字段
|
||||
dto.setGunCode(gun.getPileConnectorCode());
|
||||
dto.setGunName(gun.getName());
|
||||
dto.setPileCode(gun.getPileSn());
|
||||
|
||||
// 提取枪号(最后 2 位)
|
||||
String gunNo = extractGunNo(gun.getPileConnectorCode());
|
||||
dto.setGunNo(gunNo);
|
||||
|
||||
// 构建附加信息
|
||||
JSONObject additionalInfo = new JSONObject();
|
||||
additionalInfo.put("webGunId", gun.getId());
|
||||
additionalInfo.put("status", gun.getStatus());
|
||||
additionalInfo.put("parkNo", gun.getParkNo());
|
||||
|
||||
dto.setAdditionalInfo(additionalInfo);
|
||||
|
||||
dtoList.add(dto);
|
||||
}
|
||||
|
||||
return dtoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从充电枪编码中提取枪号(最后 2 位)
|
||||
*/
|
||||
private String extractGunNo(String gunCode) {
|
||||
if (StringUtils.isEmpty(gunCode) || gunCode.length() < 2) {
|
||||
return "01"; // 默认值
|
||||
}
|
||||
return gunCode.substring(gunCode.length() - 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 JCPP 同步接口
|
||||
*/
|
||||
private JcppSyncResponse callJcppSyncApi(List<JcppPileSyncDTO> pileDTOs, List<JcppGunSyncDTO> gunDTOs) {
|
||||
List<JcppSyncResult> pileResults = new ArrayList<>();
|
||||
List<JcppSyncResult> gunResults = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 1. 同步充电桩
|
||||
if (pileDTOs != null && !pileDTOs.isEmpty()) {
|
||||
pileResults = syncPilesToJcpp(pileDTOs);
|
||||
}
|
||||
|
||||
// 2. 同步充电枪
|
||||
if (gunDTOs != null && !gunDTOs.isEmpty()) {
|
||||
gunResults = syncGunsToJcpp(gunDTOs);
|
||||
}
|
||||
|
||||
// 3. 构建响应
|
||||
return JcppSyncResponse.build(pileResults, gunResults);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用 JCPP 同步接口失败", e);
|
||||
throw new RuntimeException("调用 JCPP 同步接口失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步充电桩到 JCPP
|
||||
*/
|
||||
private List<JcppSyncResult> syncPilesToJcpp(List<JcppPileSyncDTO> pileDTOs) {
|
||||
String url = jcppApiUrl + "/piles";
|
||||
|
||||
// 构建请求体
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.put("piles", pileDTOs);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
|
||||
|
||||
try {
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
// 解析响应
|
||||
JSONObject responseBody = JSON.parseObject(response.getBody());
|
||||
List<JcppSyncResult> results = responseBody.getList("results", JcppSyncResult.class);
|
||||
return results != null ? results : new ArrayList<>();
|
||||
} else {
|
||||
log.error("JCPP 充电桩同步接口返回错误: {}", response.getStatusCode());
|
||||
// 返回失败结果
|
||||
List<JcppSyncResult> results = new ArrayList<>();
|
||||
for (JcppPileSyncDTO dto : pileDTOs) {
|
||||
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口返回错误: " + response.getStatusCode()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用 JCPP 充电桩同步接口异常", e);
|
||||
// 返回失败结果
|
||||
List<JcppSyncResult> results = new ArrayList<>();
|
||||
for (JcppPileSyncDTO dto : pileDTOs) {
|
||||
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口调用异常: " + e.getMessage()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步充电枪到 JCPP
|
||||
*/
|
||||
private List<JcppSyncResult> syncGunsToJcpp(List<JcppGunSyncDTO> gunDTOs) {
|
||||
String url = jcppApiUrl + "/guns";
|
||||
|
||||
// 构建请求体
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.put("guns", gunDTOs);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
|
||||
|
||||
try {
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
// 解析响应
|
||||
JSONObject responseBody = JSON.parseObject(response.getBody());
|
||||
List<JcppSyncResult> results = responseBody.getList("results", JcppSyncResult.class);
|
||||
return results != null ? results : new ArrayList<>();
|
||||
} else {
|
||||
log.error("JCPP 充电枪同步接口返回错误: {}", response.getStatusCode());
|
||||
// 返回失败结果
|
||||
List<JcppSyncResult> results = new ArrayList<>();
|
||||
for (JcppGunSyncDTO dto : gunDTOs) {
|
||||
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口返回错误: " + response.getStatusCode()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用 JCPP 充电枪同步接口异常", e);
|
||||
// 返回失败结果
|
||||
List<JcppSyncResult> results = new ArrayList<>();
|
||||
for (JcppGunSyncDTO dto : gunDTOs) {
|
||||
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口调用异常: " + e.getMessage()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建同步记录
|
||||
*/
|
||||
private JcppSyncRecord createSyncRecord(String syncType) {
|
||||
JcppSyncRecord record = new JcppSyncRecord();
|
||||
record.setSyncType(syncType);
|
||||
record.setSyncStatus("RUNNING");
|
||||
record.setStartTime(new Date());
|
||||
jcppSyncRecordMapper.insertJcppSyncRecord(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新同步记录(成功)
|
||||
*/
|
||||
private void updateSyncRecord(JcppSyncRecord record, JcppSyncResponse response, String status) {
|
||||
updateSyncRecord(record, response, status, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新同步记录
|
||||
*/
|
||||
private void updateSyncRecord(JcppSyncRecord record, JcppSyncResponse response, String status, String errorMessage) {
|
||||
record.setSyncStatus(status);
|
||||
record.setEndTime(new Date());
|
||||
|
||||
if (response != null) {
|
||||
record.setTotalPiles(response.getTotalPiles());
|
||||
record.setSuccessPiles(response.getSuccessPiles());
|
||||
record.setFailedPiles(response.getFailedPiles());
|
||||
record.setTotalGuns(response.getTotalGuns());
|
||||
record.setSuccessGuns(response.getSuccessGuns());
|
||||
record.setFailedGuns(response.getFailedGuns());
|
||||
|
||||
if (response.getErrors() != null && !response.getErrors().isEmpty()) {
|
||||
record.setErrorMessage(String.join("; ", response.getErrors()));
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != null) {
|
||||
record.setErrorMessage(errorMessage);
|
||||
}
|
||||
|
||||
jcppSyncRecordMapper.updateJcppSyncRecord(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.jsowell.pile.jcpp.util;
|
||||
|
||||
import com.google.common.hash.Hashing;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* JCPP 消息分区计算器
|
||||
* 使用 MurmurHash3_128 算法计算消息分区,与 JCPP 保持一致
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Slf4j
|
||||
public class JcppPartitionCalculator {
|
||||
|
||||
/**
|
||||
* 默认分区数量
|
||||
*/
|
||||
private static final int DEFAULT_PARTITION_COUNT = 10;
|
||||
|
||||
/**
|
||||
* 分区数量(可配置)
|
||||
*/
|
||||
private static int partitionCount = DEFAULT_PARTITION_COUNT;
|
||||
|
||||
/**
|
||||
* 设置分区数量
|
||||
*
|
||||
* @param count 分区数量
|
||||
*/
|
||||
public static void setPartitionCount(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("分区数量必须大于 0");
|
||||
}
|
||||
partitionCount = count;
|
||||
log.info("JCPP 消息分区数量设置为: {}", partitionCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前分区数量
|
||||
*
|
||||
* @return 分区数量
|
||||
*/
|
||||
public static int getPartitionCount() {
|
||||
return partitionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息键计算分区编号
|
||||
* 使用 MurmurHash3_128 算法(与 JCPP 一致)
|
||||
*
|
||||
* @param messageKey 消息键(通常是 pileCode)
|
||||
* @return 分区编号(0 到 partitionCount-1)
|
||||
*/
|
||||
public static int getPartition(String messageKey) {
|
||||
if (messageKey == null || messageKey.isEmpty()) {
|
||||
log.warn("消息键为空,使用默认分区 0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 使用 MurmurHash3_128 算法计算 hash 值
|
||||
long hash = Hashing.murmur3_128()
|
||||
.hashString(messageKey, StandardCharsets.UTF_8)
|
||||
.asLong();
|
||||
|
||||
// 取绝对值并对分区数取模
|
||||
int partition = Math.abs((int) (hash % partitionCount));
|
||||
|
||||
log.debug("消息键: {}, hash: {}, 分区: {}", messageKey, hash, partition);
|
||||
|
||||
return partition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分区的队列名称
|
||||
*
|
||||
* @param partition 分区编号
|
||||
* @return 队列名称
|
||||
*/
|
||||
public static String getQueueName(int partition) {
|
||||
return "jcpp.uplink.partition." + partition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分区的队列名称数组
|
||||
*
|
||||
* @return 队列名称数组
|
||||
*/
|
||||
public static String[] getAllQueueNames() {
|
||||
String[] queueNames = new String[partitionCount];
|
||||
for (int i = 0; i < partitionCount; i++) {
|
||||
queueNames[i] = getQueueName(i);
|
||||
}
|
||||
return queueNames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.jsowell.pile.mapper;
|
||||
|
||||
import com.jsowell.pile.domain.JcppSyncRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JCPP 充电桩同步记录 Mapper 接口
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public interface JcppSyncRecordMapper {
|
||||
|
||||
/**
|
||||
* 查询 JCPP 充电桩同步记录
|
||||
*
|
||||
* @param id JCPP 充电桩同步记录主键
|
||||
* @return JCPP 充电桩同步记录
|
||||
*/
|
||||
JcppSyncRecord selectJcppSyncRecordById(Long id);
|
||||
|
||||
/**
|
||||
* 查询 JCPP 充电桩同步记录列表
|
||||
*
|
||||
* @param jcppSyncRecord JCPP 充电桩同步记录
|
||||
* @return JCPP 充电桩同步记录集合
|
||||
*/
|
||||
List<JcppSyncRecord> selectJcppSyncRecordList(JcppSyncRecord jcppSyncRecord);
|
||||
|
||||
/**
|
||||
* 新增 JCPP 充电桩同步记录
|
||||
*
|
||||
* @param jcppSyncRecord JCPP 充电桩同步记录
|
||||
* @return 结果
|
||||
*/
|
||||
int insertJcppSyncRecord(JcppSyncRecord jcppSyncRecord);
|
||||
|
||||
/**
|
||||
* 修改 JCPP 充电桩同步记录
|
||||
*
|
||||
* @param jcppSyncRecord JCPP 充电桩同步记录
|
||||
* @return 结果
|
||||
*/
|
||||
int updateJcppSyncRecord(JcppSyncRecord jcppSyncRecord);
|
||||
|
||||
/**
|
||||
* 删除 JCPP 充电桩同步记录
|
||||
*
|
||||
* @param id JCPP 充电桩同步记录主键
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteJcppSyncRecordById(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除 JCPP 充电桩同步记录
|
||||
*
|
||||
* @param ids 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteJcppSyncRecordByIds(Long[] ids);
|
||||
|
||||
/**
|
||||
* 查询最后一次成功的同步记录
|
||||
*
|
||||
* @param syncType 同步类型
|
||||
* @return 同步记录
|
||||
*/
|
||||
JcppSyncRecord selectLastSuccessRecord(String syncType);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.pile.mapper.JcppSyncRecordMapper">
|
||||
|
||||
<resultMap type="com.jsowell.pile.domain.JcppSyncRecord" id="JcppSyncRecordResult">
|
||||
<result property="id" column="id" />
|
||||
<result property="syncType" column="sync_type" />
|
||||
<result property="syncStatus" column="sync_status" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="totalPiles" column="total_piles" />
|
||||
<result property="successPiles" column="success_piles" />
|
||||
<result property="failedPiles" column="failed_piles" />
|
||||
<result property="totalGuns" column="total_guns" />
|
||||
<result property="successGuns" column="success_guns" />
|
||||
<result property="failedGuns" column="failed_guns" />
|
||||
<result property="errorMessage" column="error_message" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectJcppSyncRecordVo">
|
||||
select id, sync_type, sync_status, start_time, end_time, total_piles, success_piles, failed_piles, total_guns, success_guns, failed_guns, error_message, create_by, create_time from jcpp_sync_record
|
||||
</sql>
|
||||
|
||||
<select id="selectJcppSyncRecordList" parameterType="com.jsowell.pile.domain.JcppSyncRecord" resultMap="JcppSyncRecordResult">
|
||||
<include refid="selectJcppSyncRecordVo"/>
|
||||
<where>
|
||||
<if test="syncType != null and syncType != ''"> and sync_type = #{syncType}</if>
|
||||
<if test="syncStatus != null and syncStatus != ''"> and sync_status = #{syncStatus}</if>
|
||||
<if test="startTime != null "> and start_time >= #{startTime}</if>
|
||||
<if test="endTime != null "> and end_time <= #{endTime}</if>
|
||||
</where>
|
||||
order by start_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectJcppSyncRecordById" parameterType="Long" resultMap="JcppSyncRecordResult">
|
||||
<include refid="selectJcppSyncRecordVo"/>
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectLastSuccessRecord" parameterType="String" resultMap="JcppSyncRecordResult">
|
||||
<include refid="selectJcppSyncRecordVo"/>
|
||||
where sync_type = #{syncType} and sync_status = 'SUCCESS'
|
||||
order by start_time desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertJcppSyncRecord" parameterType="com.jsowell.pile.domain.JcppSyncRecord" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into jcpp_sync_record
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="syncType != null and syncType != ''">sync_type,</if>
|
||||
<if test="syncStatus != null and syncStatus != ''">sync_status,</if>
|
||||
<if test="startTime != null">start_time,</if>
|
||||
<if test="endTime != null">end_time,</if>
|
||||
<if test="totalPiles != null">total_piles,</if>
|
||||
<if test="successPiles != null">success_piles,</if>
|
||||
<if test="failedPiles != null">failed_piles,</if>
|
||||
<if test="totalGuns != null">total_guns,</if>
|
||||
<if test="successGuns != null">success_guns,</if>
|
||||
<if test="failedGuns != null">failed_guns,</if>
|
||||
<if test="errorMessage != null">error_message,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="syncType != null and syncType != ''">#{syncType},</if>
|
||||
<if test="syncStatus != null and syncStatus != ''">#{syncStatus},</if>
|
||||
<if test="startTime != null">#{startTime},</if>
|
||||
<if test="endTime != null">#{endTime},</if>
|
||||
<if test="totalPiles != null">#{totalPiles},</if>
|
||||
<if test="successPiles != null">#{successPiles},</if>
|
||||
<if test="failedPiles != null">#{failedPiles},</if>
|
||||
<if test="totalGuns != null">#{totalGuns},</if>
|
||||
<if test="successGuns != null">#{successGuns},</if>
|
||||
<if test="failedGuns != null">#{failedGuns},</if>
|
||||
<if test="errorMessage != null">#{errorMessage},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateJcppSyncRecord" parameterType="com.jsowell.pile.domain.JcppSyncRecord">
|
||||
update jcpp_sync_record
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="syncType != null and syncType != ''">sync_type = #{syncType},</if>
|
||||
<if test="syncStatus != null and syncStatus != ''">sync_status = #{syncStatus},</if>
|
||||
<if test="startTime != null">start_time = #{startTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="totalPiles != null">total_piles = #{totalPiles},</if>
|
||||
<if test="successPiles != null">success_piles = #{successPiles},</if>
|
||||
<if test="failedPiles != null">failed_piles = #{failedPiles},</if>
|
||||
<if test="totalGuns != null">total_guns = #{totalGuns},</if>
|
||||
<if test="successGuns != null">success_guns = #{successGuns},</if>
|
||||
<if test="failedGuns != null">failed_guns = #{failedGuns},</if>
|
||||
<if test="errorMessage != null">error_message = #{errorMessage},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteJcppSyncRecordById" parameterType="Long">
|
||||
delete from jcpp_sync_record where id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteJcppSyncRecordByIds" parameterType="String">
|
||||
delete from jcpp_sync_record where id in
|
||||
<foreach item="id" collection="array" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
10
pom.xml
10
pom.xml
@@ -33,7 +33,8 @@
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<jwt.version>0.9.1</jwt.version>
|
||||
<lombok.version>1.18.24</lombok.version>
|
||||
<guava.version>20.0</guava.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<protobuf.version>3.25.1</protobuf.version>
|
||||
<spring.boot.version>2.5.14</spring.boot.version>
|
||||
<netty-all.version>4.1.75.Final</netty-all.version>
|
||||
<mqttv3.version>1.2.5</mqttv3.version>
|
||||
@@ -353,6 +354,13 @@
|
||||
<version>${protostuff.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Google Protobuf -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jsowell</groupId>
|
||||
<artifactId>charge-common-api</artifactId>
|
||||
|
||||
21
sql/jcpp_sync_record.sql
Normal file
21
sql/jcpp_sync_record.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- JCPP 充电桩同步记录表
|
||||
CREATE TABLE `jcpp_sync_record` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`sync_type` varchar(20) NOT NULL COMMENT '同步类型(FULL-全量;INCREMENTAL-增量)',
|
||||
`sync_status` varchar(20) NOT NULL COMMENT '同步状态(RUNNING-进行中;SUCCESS-成功;FAILED-失败)',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`total_piles` int(11) DEFAULT 0 COMMENT '总充电桩数',
|
||||
`success_piles` int(11) DEFAULT 0 COMMENT '成功充电桩数',
|
||||
`failed_piles` int(11) DEFAULT 0 COMMENT '失败充电桩数',
|
||||
`total_guns` int(11) DEFAULT 0 COMMENT '总充电枪数',
|
||||
`success_guns` int(11) DEFAULT 0 COMMENT '成功充电枪数',
|
||||
`failed_guns` int(11) DEFAULT 0 COMMENT '失败充电枪数',
|
||||
`error_message` text COMMENT '错误信息',
|
||||
`create_by` varchar(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_sync_type` (`sync_type`) USING BTREE,
|
||||
KEY `idx_start_time` (`start_time`) USING BTREE,
|
||||
KEY `idx_sync_status` (`sync_status`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='JCPP 充电桩同步记录表';
|
||||
Reference in New Issue
Block a user