同步充电桩数据

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,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);
}

View File

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

View File

@@ -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"; // 离网
}
}
}

View File

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