同步充电桩数据

This commit is contained in:
Guoqs
2025-12-30 15:59:34 +08:00
parent 3f42441869
commit ee7a3425d0
38 changed files with 4663 additions and 131 deletions

View 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.1JSON 格式)
**最后更新**2025-12-30
**维护人员**jsowell 团队

View File

@@ -0,0 +1,710 @@
# JCPP 充电桩平台对接进度文档
> 文档创建时间2025-12-30
> 最后更新时间2025-12-30
> 对接平台JCPPJava 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] 将实时数据缓存到 Redis5 分钟过期)
- [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`
- 绑定队列到 Exchangerouting 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)
---
## 十、变更记录

View File

@@ -0,0 +1,359 @@
# JCPP 项目充电桩数据同步接口实现 Prompt
> 请将以下内容复制给 JCPP 项目的 Claude Code 执行
---
## 任务概述
需要在 JCPP 项目中实现充电桩数据同步接口,用于接收来自 Web 项目的充电桩和充电枪数据。
## 背景说明
Web 项目MySQL中维护了充电桩的主数据现在需要将这些数据同步到 JCPP 项目PostgreSQL用于登录鉴权等操作。
### 数据流向
```
Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
```
### 关键差异
1. **主键类型**Web 使用 intJCPP 使用 uuid
2. **station_id 类型**Web 是 intJCPP 是 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` 的充电站记录
- 如果不存在,需要先创建该充电站记录
---
## 问题反馈
如有任何问题或需要澄清的地方,请及时反馈。

View 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 (1OPERATION, 2PERSONAL)
- 其他字段 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 团队

View 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 关键问题
#### 问题1station_id 类型映射int → uuid
**解决方案**
- ✅ 统一使用固定的 UUID 值:`88bca8da-cdbf-6587-aecc-75784838c501`
- 所有充电桩都使用这个固定的 station_id
- Web 的原始 station_id 保存在 additional_info 中便于追溯
#### 问题2pile_id 获取
**解决方案**
- 先同步充电桩,获取返回的 pile_id
- 再同步充电枪时,通过 pile_code 查询 pile_id
#### 问题3gun_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)

View 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.5yunkuaichongV160--云快充V1.6yonglianV1--永联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
);
~~~