diff --git a/doc/JCPP项目配合实现Prompt.md b/doc/JCPP项目配合实现Prompt.md index 0561b567b..cabd415a0 100644 --- a/doc/JCPP项目配合实现Prompt.md +++ b/doc/JCPP项目配合实现Prompt.md @@ -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 piles) { - 考虑使用缓存优化映射查询 5. **安全性** - - 添加接口鉴权 + - 添加接口鉴权(JWT Token) - 验证请求数据的合法性 - 防止 SQL 注入 + - token 有效期 30 分钟 --- diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/JcppPileSyncController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/JcppPileSyncController.java index 57ca26f17..20b6394af 100644 --- a/jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/JcppPileSyncController.java +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/jcpp/JcppPileSyncController.java @@ -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()); } } diff --git a/jsowell-admin/src/main/resources/application-sit.yml b/jsowell-admin/src/main/resources/application-sit.yml index bab12acd6..66b20a69a 100644 --- a/jsowell-admin/src/main/resources/application-sit.yml +++ b/jsowell-admin/src/main/resources/application-sit.yml @@ -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: diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/impl/JcppPileSyncServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/impl/JcppPileSyncServiceImpl.java index 23523992c..05c6a50fe 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/impl/JcppPileSyncServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/jcpp/service/impl/JcppPileSyncServiceImpl.java @@ -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 gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun); // 3. 转换数据格式 - List pileDTOs = convertPilesToDTO(List.of(pile)); + List pileDTOs = convertPilesToDTO(Lists.newArrayList(pile)); List 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 syncPilesToJcpp(List pileDTOs) { String url = jcppApiUrl + "/piles"; - // 构建请求体 - JSONObject requestBody = new JSONObject(); - requestBody.put("piles", pileDTOs); - - // 设置请求头 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity entity = new HttpEntity<>(requestBody.toJSONString(), headers); - try { + // 获取访问令牌 + String token = jcppAuthService.getAccessToken(); + if (token == null || token.isEmpty()) { + log.error("无法获取 JCPP 访问令牌"); + // 返回失败结果 + List 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 entity = new HttpEntity<>(requestBody.toJSONString(), headers); + // 发送请求 ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); @@ -352,6 +370,12 @@ public class JcppPileSyncServiceImpl implements IJcppPileSyncService { JSONObject responseBody = JSON.parseObject(response.getBody()); List 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 retrySyncPilesToJcpp(List pileDTOs) { + String url = jcppApiUrl + "/piles"; + + try { + // 重新获取访问令牌 + String token = jcppAuthService.getAccessToken(); + if (token == null || token.isEmpty()) { + log.error("重试时仍无法获取 JCPP 访问令牌"); + // 返回失败结果 + List 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 entity = new HttpEntity<>(requestBody.toJSONString(), headers); + + // 发送请求 + ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); + + if (response.getStatusCode() == HttpStatus.OK) { + // 解析响应 + JSONObject responseBody = JSON.parseObject(response.getBody()); + List results = responseBody.getList("results", JcppSyncResult.class); + return results != null ? results : new ArrayList<>(); + } else { + log.error("JCPP 充电桩同步接口返回错误: {}", response.getStatusCode()); + // 返回失败结果 + List 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 results = new ArrayList<>(); + for (JcppPileSyncDTO dto : pileDTOs) { + results.add(JcppSyncResult.fail(dto.getPileCode(), "接口调用异常: " + e.getMessage())); + } + return results; + } + } + /** * 同步充电枪到 JCPP */ private List syncGunsToJcpp(List 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 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 entity = new HttpEntity<>(requestBody.toJSONString(), headers); + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + token); + + HttpEntity entity = new HttpEntity<>(requestBody.toJSONString(), headers); + + // 发送请求 + ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); + + if (response.getStatusCode() == HttpStatus.OK) { + // 解析响应 + JSONObject responseBody = JSON.parseObject(response.getBody()); + List 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 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 results = new ArrayList<>(); + for (JcppGunSyncDTO dto : gunDTOs) { + results.add(JcppSyncResult.fail(dto.getGunCode(), "接口调用异常: " + e.getMessage())); + } + return results; + } + } + + /** + * 重试同步充电枪(token 过期时使用) + */ + private List retrySyncGunsToJcpp(List gunDTOs) { + String url = jcppApiUrl + "/guns"; try { + // 重新获取访问令牌 + String token = jcppAuthService.getAccessToken(); + if (token == null || token.isEmpty()) { + log.error("重试时仍无法获取 JCPP 访问令牌"); + // 返回失败结果 + List 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 entity = new HttpEntity<>(requestBody.toJSONString(), headers); + // 发送请求 ResponseEntity 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 results = new ArrayList<>(); for (JcppGunSyncDTO dto : gunDTOs) { diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/JcppSyncRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/JcppSyncRecordMapper.java index 8f6b62504..f9b64c017 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/JcppSyncRecordMapper.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/JcppSyncRecordMapper.java @@ -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 { /**