同步充电桩数据

This commit is contained in:
Guoqs
2025-12-31 16:38:27 +08:00
parent ee7a3425d0
commit 193470ffb4
5 changed files with 253 additions and 43 deletions

View File

@@ -26,10 +26,39 @@ Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
## 需要实现的功能
### 1. 充电桩同步接口
### 1. 认证接口
**接口路径**`POST /api/auth/login`
**请求格式**
```json
{
"username": "sanbing",
"password": "password123"
}
```
**响应格式**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 1800
}
```
**说明**
- 用于 Web 项目获取访问令牌
- token 有效期 30 分钟
- Web 项目会将 token 缓存在 Redis 中
---
### 2. 充电桩同步接口
**接口路径**`POST /api/sync/piles`
**认证方式**Bearer Token在请求头中添加 `Authorization: Bearer {token}`
**重要说明**
- ✅ 所有充电桩的 `station_id` 统一使用固定值:`88bca8da-cdbf-6587-aecc-75784838c501`
- Web 项目的原始 station_id 保存在 `additionalInfo.webStationId` 中便于追溯
@@ -105,13 +134,17 @@ Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
- 使用事务保证数据一致性
- pile_code 必须唯一(已有唯一索引)
- 所有充电桩的 station_id 都使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
- **需要验证 Authorization 请求头中的 token**
- token 无效时返回 401 Unauthorized
---
### 2. 充电枪同步接口
### 3. 充电枪同步接口
**接口路径**`POST /api/sync/guns`
**认证方式**Bearer Token在请求头中添加 `Authorization: Bearer {token}`
**重要说明**
- ✅ 所有充电枪的 `station_id` 统一使用固定值:`88bca8da-cdbf-6587-aecc-75784838c501`
@@ -173,6 +206,8 @@ Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
- gun_code 必须唯一(已有唯一索引)
- (pile_id, gun_no) 组合必须唯一(已有唯一索引)
- 所有充电枪的 station_id 都使用固定值 `88bca8da-cdbf-6587-aecc-75784838c501`
- **需要验证 Authorization 请求头中的 token**
- token 无效时返回 401 Unauthorized
---
@@ -189,11 +224,14 @@ Web 项目 (MySQL) → HTTP API → JCPP 项目 (PostgreSQL)
```
src/main/java/com/jcpp/
├── controller/
│ ├── AuthController.java # 认证接口 Controller
│ └── SyncController.java # 同步接口 Controller
├── service/
│ ├── AuthService.java # 认证服务接口
│ ├── PileSyncService.java # 充电桩同步服务接口
│ └── GunSyncService.java # 充电枪同步服务接口
├── service/impl/
│ ├── AuthServiceImpl.java
│ ├── PileSyncServiceImpl.java
│ └── GunSyncServiceImpl.java
├── entity/
@@ -202,11 +240,15 @@ src/main/java/com/jcpp/
├── repository/
│ ├── PileRepository.java
│ └── GunRepository.java
── dto/
├── PileSyncDTO.java # 充电桩同步 DTO
├── GunSyncDTO.java # 充电枪同步 DTO
├── SyncRequest.java # 同步请求
── SyncResponse.java # 同步响应
── dto/
├── LoginRequest.java # 登录请求
├── LoginResponse.java # 登录响应
├── PileSyncDTO.java # 充电桩同步 DTO
── GunSyncDTO.java # 充电枪同步 DTO
│ ├── SyncRequest.java # 同步请求
│ └── SyncResponse.java # 同步响应
└── security/
└── JwtTokenProvider.java # JWT Token 生成和验证
```
### 关键代码示例
@@ -319,9 +361,10 @@ public SyncResponse syncPiles(List<PileSyncDTO> piles) {
- 考虑使用缓存优化映射查询
5. **安全性**
- 添加接口鉴权
- 添加接口鉴权JWT Token
- 验证请求数据的合法性
- 防止 SQL 注入
- token 有效期 30 分钟
---

View File

@@ -1,5 +1,6 @@
package com.jsowell.web.controller.jcpp;
import com.jsowell.common.annotation.Anonymous;
import com.jsowell.common.annotation.Log;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.common.core.domain.AjaxResult;
@@ -12,7 +13,6 @@ 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;
@@ -26,8 +26,9 @@ import java.util.List;
*
* @author jsowell
*/
@Slf4j
// @Slf4j
@Api(tags = "JCPP 充电桩同步")
@Anonymous
@RestController
@RequestMapping("/jcpp/sync")
public class JcppPileSyncController extends BaseController {
@@ -47,11 +48,11 @@ public class JcppPileSyncController extends BaseController {
@PostMapping("/full")
public AjaxResult syncFull() {
try {
log.info("开始执行全量同步");
logger.info("开始执行全量同步");
JcppSyncResponse response = jcppPileSyncService.syncAllPiles();
return AjaxResult.success("全量同步完成", response);
} catch (Exception e) {
log.error("全量同步失败", e);
logger.error("全量同步失败", e);
return AjaxResult.error("全量同步失败: " + e.getMessage());
}
}
@@ -69,11 +70,11 @@ public class JcppPileSyncController extends BaseController {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
Date lastSyncTime) {
try {
log.info("开始执行增量同步,上次同步时间: {}", lastSyncTime);
logger.info("开始执行增量同步,上次同步时间: {}", lastSyncTime);
JcppSyncResponse response = jcppPileSyncService.syncIncrementalPiles(lastSyncTime);
return AjaxResult.success("增量同步完成", response);
} catch (Exception e) {
log.error("增量同步失败", e);
logger.error("增量同步失败", e);
return AjaxResult.error("增量同步失败: " + e.getMessage());
}
}
@@ -89,7 +90,7 @@ public class JcppPileSyncController extends BaseController {
@ApiParam("充电桩编号")
@PathVariable String pileSn) {
try {
log.info("开始同步单个充电桩: {}", pileSn);
logger.info("开始同步单个充电桩: {}", pileSn);
boolean success = jcppPileSyncService.syncSinglePile(pileSn);
if (success) {
return AjaxResult.success("同步成功");
@@ -97,7 +98,7 @@ public class JcppPileSyncController extends BaseController {
return AjaxResult.error("同步失败");
}
} catch (Exception e) {
log.error("同步单个充电桩失败: {}", pileSn, e);
logger.error("同步单个充电桩失败: {}", pileSn, e);
return AjaxResult.error("同步失败: " + e.getMessage());
}
}

View File

@@ -14,7 +14,7 @@ jcpp:
queue-prefix: jcpp.uplink.partition
sync:
# JCPP 同步接口地址
api-url: http://localhost:8080/api/sync
api-url: http://localhost:8180/api/sync
# 批量同步大小
batch-size: 100
# 超时时间(毫秒)
@@ -23,6 +23,11 @@ jcpp:
auto-sync-enabled: false
# 自动同步间隔(分钟)
auto-sync-interval: 30
auth:
# JCPP 认证用户名
username: sanbing
# JCPP 认证密码
password: password123
# 数据源配置
spring:

View File

@@ -2,11 +2,13 @@ package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Lists;
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.IJcppAuthService;
import com.jsowell.pile.jcpp.service.IJcppPileSyncService;
import com.jsowell.pile.mapper.JcppSyncRecordMapper;
import com.jsowell.pile.service.PileBasicInfoService;
@@ -40,10 +42,13 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
@Autowired
private JcppSyncRecordMapper jcppSyncRecordMapper;
@Autowired
private IJcppAuthService jcppAuthService;
@Autowired
private RestTemplate restTemplate;
@Value("${jcpp.sync.api-url:http://localhost:8080/api/sync}")
@Value("${jcpp.sync.api-url:http://localhost:8180/api/sync}")
private String jcppApiUrl;
@Value("${jcpp.sync.batch-size:100}")
@@ -196,7 +201,7 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
List<PileConnectorInfo> gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
// 3. 转换数据格式
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(List.of(pile));
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(Lists.newArrayList(pile));
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
// 4. 调用 JCPP 同步接口
@@ -227,9 +232,9 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
dto.setProtocol(pile.getSoftwareProtocol());
// 品牌、型号、制造商(可为空)
dto.setBrand(null); // Web 项目中没有这些字段
dto.setBrand("jsowell"); // Web 项目中没有这些字段
dto.setModel(null);
dto.setManufacturer(null);
dto.setManufacturer("jsowell");
// 类型映射1-运营桩 → OPERATION, 2-个人桩 → PERSONAL
String type = "OPERATION"; // 默认运营桩
@@ -244,11 +249,11 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
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("longitude", pile.getLongitude());
// additionalInfo.put("latitude", pile.getLatitude());
additionalInfo.put("iccid", pile.getIccId());
additionalInfo.put("merchantId", pile.getMerchantId());
additionalInfo.put("vinFlag", pile.getVinFlag());
// additionalInfo.put("vinFlag", pile.get());
dto.setAdditionalInfo(additionalInfo);
@@ -333,17 +338,30 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
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 {
// 获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("piles", pileDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
@@ -352,6 +370,12 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
JSONObject responseBody = JSON.parseObject(response.getBody());
List<JcppSyncResult> results = responseBody.getList("results", JcppSyncResult.class);
return results != null ? results : new ArrayList<>();
} else if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// token 过期,清除缓存并重试一次
log.warn("JCPP 访问令牌已过期,清除缓存并重试");
jcppAuthService.clearToken();
// 递归调用重试(只重试一次)
return retrySyncPilesToJcpp(pileDTOs);
} else {
log.error("JCPP 充电桩同步接口返回错误: {}", response.getStatusCode());
// 返回失败结果
@@ -372,23 +396,158 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
}
}
/**
* 重试同步充电桩token 过期时使用)
*/
private List<JcppSyncResult> retrySyncPilesToJcpp(List<JcppPileSyncDTO> pileDTOs) {
String url = jcppApiUrl + "/piles";
try {
// 重新获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("重试时仍无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("piles", pileDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
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);
try {
// 获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "无法获取访问令牌"));
}
return results;
}
// 设置请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 构建请求
JSONObject requestBody = new JSONObject();
requestBody.put("guns", gunDTOs);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
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 if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// token 过期,清除缓存并重试一次
log.warn("JCPP 访问令牌已过期,清除缓存并重试");
jcppAuthService.clearToken();
// 递归调用重试(只重试一次)
return retrySyncGunsToJcpp(gunDTOs);
} 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;
}
}
/**
* 重试同步充电枪token 过期时使用)
*/
private List<JcppSyncResult> retrySyncGunsToJcpp(List<JcppGunSyncDTO> gunDTOs) {
String url = jcppApiUrl + "/guns";
try {
// 重新获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("重试时仍无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("guns", gunDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
@@ -407,7 +566,7 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
return results;
}
} catch (Exception e) {
log.error("调用 JCPP 充电枪同步接口异常", e);
log.error("重试调用 JCPP 充电枪同步接口异常", e);
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {

View File

@@ -1,6 +1,7 @@
package com.jsowell.pile.mapper;
import com.jsowell.pile.domain.JcppSyncRecord;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -9,6 +10,7 @@ import java.util.List;
*
* @author jsowell
*/
@Repository
public interface JcppSyncRecordMapper {
/**