对接jcpp

This commit is contained in:
Guoqs
2025-12-27 15:05:41 +08:00
parent bc3e8091de
commit 7d170dbe64
35 changed files with 5185 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
package com.jsowell.web.controller.jcpp;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.common.core.domain.AjaxResult;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.PileBillingTemplate;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.util.PricingModelConverter;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileBillingTemplateService;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* JCPP 计费模板接口
*
* @author jsowell
*/
// @Slf4j
@Api(tags = "JCPP计费模板接口")
@RestController
@RequestMapping("/api/jcpp")
public class JcppBillingController extends BaseController {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
/**
* 获取充电桩计费模板
*
* @param pileCode 充电桩编码
* @return 计费模板
*/
@ApiOperation("获取充电桩计费模板")
@GetMapping("/billing/{pileCode}")
public AjaxResult getBillingTemplate(
@ApiParam(value = "充电桩编码", required = true) @PathVariable String pileCode) {
try {
logger.info("查询充电桩计费模板: pileCode={}", pileCode);
// 1. 根据 pileCode 查询充电桩
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
logger.warn("充电桩不存在: pileCode={}", pileCode);
return AjaxResult.error(404, "充电桩不存在");
}
// 2. 获取关联的计费模板 ID
BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
if (billingTemplateVO == null || billingTemplateVO.getTemplateId() == null) {
logger.warn("充电桩未配置计费模板: pileCode={}", pileCode);
return AjaxResult.error(404, "未配置计费模板");
}
Long billingTemplateId = Long.parseLong(billingTemplateVO.getTemplateId());
// 3. 查询计费模板
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(billingTemplateId);
if (template == null) {
logger.warn("计费模板不存在: pileCode={}, billingTemplateId={}", pileCode, billingTemplateId);
return AjaxResult.error(404, "计费模板不存在");
}
// 4. 转换为 JCPP 格式
JcppPricingModel pricingModel = PricingModelConverter.convert(template);
// 5. 构建返回结果
Map<String, Object> data = new HashMap<>();
data.put("pricingId", billingTemplateId);
data.put("pricingModel", pricingModel);
logger.info("查询计费模板成功: pileCode={}, pricingId={}", pileCode, billingTemplateId);
return AjaxResult.success(data);
} catch (Exception e) {
logger.error("查询计费模板异常: pileCode={}", pileCode, e);
return AjaxResult.error("查询计费模板失败: " + e.getMessage());
}
}
/**
* 根据计费模板ID获取计费模板
*
* @param pricingId 计费模板ID
* @return 计费模板
*/
@ApiOperation("根据ID获取计费模板")
@GetMapping("/billing/id/{pricingId}")
public AjaxResult getBillingTemplateById(
@ApiParam(value = "计费模板ID", required = true) @PathVariable Long pricingId) {
try {
logger.info("查询计费模板: pricingId={}", pricingId);
// 查询计费模板
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(pricingId);
if (template == null) {
logger.warn("计费模板不存在: pricingId={}", pricingId);
return AjaxResult.error(404, "计费模板不存在");
}
// 转换为 JCPP 格式
JcppPricingModel pricingModel = PricingModelConverter.convert(template);
// 构建返回结果
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
logger.info("查询计费模板成功: pricingId={}", pricingId);
return AjaxResult.success(data);
} catch (Exception e) {
logger.error("查询计费模板异常: pricingId={}", pricingId, e);
return AjaxResult.error("查询计费模板失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,61 @@
package com.jsowell.web.controller.jcpp;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.pile.jcpp.config.JcppConfig;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.dto.JcppUplinkResponse;
import com.jsowell.pile.jcpp.service.IJcppMessageService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* JCPP 消息接收 Controller
* 用于接收 JCPP 项目推送的充电桩上行消息
*
* @author jsowell
*/
// @Slf4j
@Api(tags = "JCPP消息接收接口")
@RestController
@RequestMapping("/api/jcpp")
public class JcppMessageController extends BaseController {
@Autowired
private IJcppMessageService jcppMessageService;
@Autowired
private JcppConfig jcppConfig;
/**
* 接收 JCPP 上行消息
*
* @param message 上行消息
* @return 响应
*/
@ApiOperation("接收JCPP上行消息")
@PostMapping("/uplink")
public JcppUplinkResponse receiveUplink(@RequestBody JcppUplinkMessage message) {
// 检查是否启用 JCPP 对接
if (!jcppConfig.isEnabled()) {
logger.warn("JCPP 对接未启用,忽略消息: {}", message.getMessageId());
return JcppUplinkResponse.error(message.getMessageId(), "JCPP 对接未启用");
}
logger.info("收到 JCPP 上行消息: messageId={}, messageType={}, pileCode={}, sessionId={}",
message.getMessageId(), message.getMessageType(),
message.getPileCode(), message.getSessionId());
try {
return jcppMessageService.handleMessage(message);
} catch (Exception e) {
logger.error("处理 JCPP 上行消息异常: messageId={}, error={}",
message.getMessageId(), e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理消息异常: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,85 @@
package com.jsowell.web.controller.jcpp;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.common.core.domain.AjaxResult;
import com.jsowell.pile.jcpp.service.IJcppRemoteChargeService;
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.web.bind.annotation.*;
/**
* JCPP 远程充电 Controller
* 用于 APP/小程序发起的远程启动/停止充电
*
* @author jsowell
*/
// @Slf4j
@Api(tags = "JCPP远程充电接口")
@RestController
@RequestMapping("/api/jcpp/charge")
public class JcppRemoteChargeController extends BaseController {
@Autowired
private IJcppRemoteChargeService jcppRemoteChargeService;
/**
* 远程启动充电
*/
@ApiOperation("远程启动充电")
@PostMapping("/start")
public AjaxResult startCharging(
@ApiParam(value = "会员ID", required = true) @RequestParam String memberId,
@ApiParam(value = "充电桩编码", required = true) @RequestParam String pileCode,
@ApiParam(value = "枪编号", required = true) @RequestParam String gunNo,
@ApiParam(value = "预付金额(元)", required = true) @RequestParam String payAmount) {
try {
String orderCode = jcppRemoteChargeService.remoteStartCharging(memberId, pileCode, gunNo, payAmount);
return AjaxResult.success("启动指令已发送", orderCode);
} catch (Exception e) {
logger.error("远程启动充电失败, memberId: {}, pileCode: {}, error: {}",
memberId, pileCode, e.getMessage(), e);
return AjaxResult.error(e.getMessage());
}
}
/**
* 远程停止充电
*/
@ApiOperation("远程停止充电")
@PostMapping("/stop")
public AjaxResult stopCharging(
@ApiParam(value = "会员ID", required = true) @RequestParam String memberId,
@ApiParam(value = "订单号", required = true) @RequestParam String orderCode) {
try {
boolean result = jcppRemoteChargeService.remoteStopCharging(memberId, orderCode);
if (result) {
return AjaxResult.success("停止指令已发送");
} else {
return AjaxResult.error("停止充电失败");
}
} catch (Exception e) {
logger.error("远程停止充电失败, memberId: {}, orderCode: {}, error: {}",
memberId, orderCode, e.getMessage(), e);
return AjaxResult.error(e.getMessage());
}
}
/**
* 检查充电桩是否在线
*/
@ApiOperation("检查充电桩是否在线")
@GetMapping("/online/{pileCode}")
public AjaxResult checkOnline(
@ApiParam(value = "充电桩编码", required = true) @PathVariable String pileCode) {
try {
boolean online = jcppRemoteChargeService.isPileOnline(pileCode);
return AjaxResult.success(online);
} catch (Exception e) {
logger.error("检查充电桩在线状态失败, pileCode: {}, error: {}", pileCode, e.getMessage(), e);
return AjaxResult.error(e.getMessage());
}
}
}

View File

@@ -0,0 +1,257 @@
package com.jsowell.jcpp;
import com.alibaba.fastjson2.JSON;
import com.jsowell.pile.jcpp.dto.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* JCPP 消息接收接口测试
*
* @author jsowell
*/
@SpringBootTest
@AutoConfigureMockMvc
public class JcppMessageControllerTest {
@Autowired
private MockMvc mockMvc;
/**
* 测试登录消息处理
*/
@Test
public void testHandleLogin() throws Exception {
// 构造登录消息
JcppLoginData loginData = JcppLoginData.builder()
.pileCode("TEST001")
.credential("test-credential")
.remoteAddress("192.168.1.100:8080")
.nodeId("node-1")
.nodeHostAddress("192.168.1.1")
.nodeRestPort(8080)
.nodeGrpcPort(9090)
.build();
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-001")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("LOGIN")
.timestamp(System.currentTimeMillis())
.data(loginData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试心跳消息处理
*/
@Test
public void testHandleHeartbeat() throws Exception {
Map<String, Object> heartbeatData = new HashMap<>();
heartbeatData.put("pileCode", "TEST001");
heartbeatData.put("remoteAddress", "192.168.1.100:8080");
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-002")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("HEARTBEAT")
.timestamp(System.currentTimeMillis())
.data(heartbeatData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试查询计费模板消息处理
*/
@Test
public void testHandleQueryPricing() throws Exception {
Map<String, Object> queryData = new HashMap<>();
queryData.put("pileCode", "TEST001");
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-003")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("QUERY_PRICING")
.timestamp(System.currentTimeMillis())
.data(queryData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试刷卡启动充电消息处理
*/
@Test
public void testHandleStartCharge() throws Exception {
JcppStartChargeData startData = JcppStartChargeData.builder()
.pileCode("TEST001")
.gunNo("1")
.startType("CARD")
.cardNo("1234567890")
.needPassword(false)
.build();
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-004")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("START_CHARGE")
.timestamp(System.currentTimeMillis())
.data(startData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试实时数据消息处理
*/
@Test
public void testHandleRealTimeData() throws Exception {
JcppRealTimeData realTimeData = JcppRealTimeData.builder()
.pileCode("TEST001")
.gunNo("1")
.tradeNo("trade-001")
.outputVoltage("380.5")
.outputCurrent("32.0")
.soc(50)
.totalChargingDurationMin(30)
.totalChargingEnergyKWh("15.5")
.totalChargingCostYuan("12.40")
.build();
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-005")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("REAL_TIME_DATA")
.timestamp(System.currentTimeMillis())
.data(realTimeData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试交易记录消息处理
*/
@Test
public void testHandleTransactionRecord() throws Exception {
JcppTransactionData transactionData = JcppTransactionData.builder()
.pileCode("TEST001")
.gunNo("1")
.tradeNo("trade-001")
.startTs(System.currentTimeMillis() - 3600000)
.endTs(System.currentTimeMillis())
.totalEnergyKWh("30.5")
.totalAmountYuan("24.40")
.tradeTs(System.currentTimeMillis())
.stopReason("USER_STOP")
.build();
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-006")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("TRANSACTION_RECORD")
.timestamp(System.currentTimeMillis())
.data(transactionData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试远程启动结果消息处理
*/
@Test
public void testHandleRemoteStartResult() throws Exception {
JcppRemoteStartResultData resultData = JcppRemoteStartResultData.builder()
.pileCode("TEST001")
.gunNo("1")
.tradeNo("trade-001")
.success(true)
.build();
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-007")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("REMOTE_START_RESULT")
.timestamp(System.currentTimeMillis())
.data(resultData)
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk());
}
/**
* 测试无效消息类型
*/
@Test
public void testHandleInvalidMessageType() throws Exception {
JcppUplinkMessage message = JcppUplinkMessage.builder()
.messageId("msg-008")
.sessionId("session-001")
.protocolName("YKC_V1.6")
.pileCode("TEST001")
.messageType("INVALID_TYPE")
.timestamp(System.currentTimeMillis())
.data(new HashMap<>())
.build();
mockMvc.perform(post("/api/jcpp/uplink")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(message)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false));
}
}

View File

@@ -0,0 +1,144 @@
package com.jsowell.jcpp;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.util.PricingModelConverter;
import com.jsowell.pile.vo.web.BillingDetailVO;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 计费模板转换工具类测试
*
* @author jsowell
*/
public class PricingModelConverterTest {
/**
* 测试标准计费模板转换
*/
@Test
public void testConvertStandardPricing() {
BillingTemplateVO template = new BillingTemplateVO();
template.setId(1L);
template.setName("标准计费模板");
List<BillingDetailVO> details = new ArrayList<>();
BillingDetailVO detail = new BillingDetailVO();
detail.setTimeType("3"); // 平时
detail.setElectricityPrice(new BigDecimal("0.8"));
detail.setServicePrice(new BigDecimal("0.4"));
detail.setApplyTime("00:00-24:00");
details.add(detail);
template.setBillingDetailList(details);
JcppPricingModel model = PricingModelConverter.convert(template);
assertNotNull(model);
assertEquals(1L, model.getPricingId());
assertEquals("标准计费模板", model.getPricingName());
assertEquals(1, model.getPricingType()); // 标准计费
assertEquals(new BigDecimal("0.8"), model.getElectricityPrice());
assertEquals(new BigDecimal("0.4"), model.getServicePrice());
}
/**
* 测试峰谷计费模板转换
*/
@Test
public void testConvertPeakValleyPricing() {
BillingTemplateVO template = new BillingTemplateVO();
template.setId(2L);
template.setName("峰谷计费模板");
List<BillingDetailVO> details = new ArrayList<>();
// 尖时
BillingDetailVO sharpDetail = new BillingDetailVO();
sharpDetail.setTimeType("1");
sharpDetail.setElectricityPrice(new BigDecimal("1.2"));
sharpDetail.setServicePrice(new BigDecimal("0.6"));
sharpDetail.setApplyTime("10:00-12:00,18:00-20:00");
details.add(sharpDetail);
// 峰时
BillingDetailVO peakDetail = new BillingDetailVO();
peakDetail.setTimeType("2");
peakDetail.setElectricityPrice(new BigDecimal("1.0"));
peakDetail.setServicePrice(new BigDecimal("0.5"));
peakDetail.setApplyTime("08:00-10:00,12:00-18:00");
details.add(peakDetail);
// 平时
BillingDetailVO flatDetail = new BillingDetailVO();
flatDetail.setTimeType("3");
flatDetail.setElectricityPrice(new BigDecimal("0.8"));
flatDetail.setServicePrice(new BigDecimal("0.4"));
flatDetail.setApplyTime("06:00-08:00,20:00-22:00");
details.add(flatDetail);
// 谷时
BillingDetailVO valleyDetail = new BillingDetailVO();
valleyDetail.setTimeType("4");
valleyDetail.setElectricityPrice(new BigDecimal("0.4"));
valleyDetail.setServicePrice(new BigDecimal("0.2"));
valleyDetail.setApplyTime("00:00-06:00,22:00-24:00");
details.add(valleyDetail);
template.setBillingDetailList(details);
JcppPricingModel model = PricingModelConverter.convert(template);
assertNotNull(model);
assertEquals(2L, model.getPricingId());
assertEquals("峰谷计费模板", model.getPricingName());
assertEquals(2, model.getPricingType()); // 峰谷计费
// 验证峰谷价格
JcppPricingModel.PeakValleyPrice peakValley = model.getPeakValleyPrice();
assertNotNull(peakValley);
assertEquals(new BigDecimal("1.2"), peakValley.getSharpElectricityPrice());
assertEquals(new BigDecimal("0.6"), peakValley.getSharpServicePrice());
assertEquals(new BigDecimal("1.0"), peakValley.getPeakElectricityPrice());
assertEquals(new BigDecimal("0.5"), peakValley.getPeakServicePrice());
assertEquals(new BigDecimal("0.8"), peakValley.getFlatElectricityPrice());
assertEquals(new BigDecimal("0.4"), peakValley.getFlatServicePrice());
assertEquals(new BigDecimal("0.4"), peakValley.getValleyElectricityPrice());
assertEquals(new BigDecimal("0.2"), peakValley.getValleyServicePrice());
// 验证时段配置
assertNotNull(peakValley.getTimePeriodConfigs());
assertFalse(peakValley.getTimePeriodConfigs().isEmpty());
}
/**
* 测试空模板转换
*/
@Test
public void testConvertNullTemplate() {
JcppPricingModel model = PricingModelConverter.convert((BillingTemplateVO) null);
assertNull(model);
}
/**
* 测试无详情模板转换
*/
@Test
public void testConvertTemplateWithoutDetails() {
BillingTemplateVO template = new BillingTemplateVO();
template.setId(3L);
template.setName("无详情模板");
template.setBillingDetailList(null);
JcppPricingModel model = PricingModelConverter.convert(template);
assertNotNull(model);
assertEquals(3L, model.getPricingId());
assertEquals(1, model.getPricingType()); // 默认标准计费
}
}

View File

@@ -0,0 +1,89 @@
package com.jsowell.pile.jcpp.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* JCPP 配置类
*
* @author jsowell
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "jcpp")
public class JcppConfig {
/**
* JCPP 服务地址
*/
private String url = "http://localhost:8080";
/**
* 下行接口路径
*/
private String downlinkPath = "/api/downlink";
/**
* 会话查询接口路径
*/
private String sessionPath = "/api/session";
/**
* 请求超时时间(毫秒)
*/
private int timeout = 5000;
/**
* 连接超时时间(毫秒)
*/
private int connectTimeout = 3000;
/**
* 是否启用 JCPP 对接
*/
private boolean enabled = true;
/**
* 重试次数
*/
private int retryCount = 3;
/**
* 重试间隔(毫秒)
*/
private int retryInterval = 1000;
/**
* 获取下行接口完整 URL
*/
public String getDownlinkUrl() {
return url + downlinkPath;
}
/**
* 获取会话查询接口完整 URL
*/
public String getSessionUrl() {
return url + sessionPath;
}
/**
* 创建 JCPP 专用的 RestTemplate
*/
@Bean("jcppRestTemplate")
public RestTemplate jcppRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(connectTimeout);
factory.setReadTimeout(timeout);
RestTemplate restTemplate = new RestTemplate(factory);
log.info("JCPP RestTemplate 初始化完成, url: {}, timeout: {}ms, connectTimeout: {}ms",
url, timeout, connectTimeout);
return restTemplate;
}
}

View File

@@ -0,0 +1,176 @@
package com.jsowell.pile.jcpp.config;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* JCPP RabbitMQ 配置类
*
* @author jsowell
*/
@Configuration
public class JcppRabbitConfig {
// ==================== Exchange ====================
/**
* 上行消息 Exchange
*/
@Bean
public TopicExchange jcppUplinkExchange() {
return new TopicExchange(JcppConstants.UPLINK_EXCHANGE, true, false);
}
// ==================== Queues ====================
/**
* 登录消息队列
*/
@Bean
public Queue jcppLoginQueue() {
return new Queue(JcppConstants.QUEUE_LOGIN, true, false, false);
}
/**
* 心跳消息队列
*/
@Bean
public Queue jcppHeartbeatQueue() {
return new Queue(JcppConstants.QUEUE_HEARTBEAT, true, false, false);
}
/**
* 启动充电消息队列
*/
@Bean
public Queue jcppStartChargeQueue() {
return new Queue(JcppConstants.QUEUE_START_CHARGE, true, false, false);
}
/**
* 实时数据消息队列
*/
@Bean
public Queue jcppRealTimeDataQueue() {
return new Queue(JcppConstants.QUEUE_REAL_TIME_DATA, true, false, false);
}
/**
* 交易记录消息队列
*/
@Bean
public Queue jcppTransactionQueue() {
return new Queue(JcppConstants.QUEUE_TRANSACTION, true, false, false);
}
/**
* 枪状态消息队列
*/
@Bean
public Queue jcppGunStatusQueue() {
return new Queue(JcppConstants.QUEUE_GUN_STATUS, true, false, false);
}
/**
* 计费模板消息队列
*/
@Bean
public Queue jcppPricingQueue() {
return new Queue(JcppConstants.QUEUE_PRICING, true, false, false);
}
/**
* 远程操作结果消息队列
*/
@Bean
public Queue jcppRemoteResultQueue() {
return new Queue(JcppConstants.QUEUE_REMOTE_RESULT, true, false, false);
}
/**
* 会话关闭消息队列
*/
@Bean
public Queue jcppSessionCloseQueue() {
return new Queue(JcppConstants.QUEUE_SESSION_CLOSE, true, false, false);
}
// ==================== Bindings ====================
/**
* 登录消息绑定
*/
@Bean
public Binding jcppLoginBinding(Queue jcppLoginQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppLoginQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_LOGIN);
}
/**
* 心跳消息绑定
*/
@Bean
public Binding jcppHeartbeatBinding(Queue jcppHeartbeatQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppHeartbeatQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_HEARTBEAT);
}
/**
* 启动充电消息绑定
*/
@Bean
public Binding jcppStartChargeBinding(Queue jcppStartChargeQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppStartChargeQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_START_CHARGE);
}
/**
* 实时数据消息绑定
*/
@Bean
public Binding jcppRealTimeDataBinding(Queue jcppRealTimeDataQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppRealTimeDataQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_REAL_TIME_DATA);
}
/**
* 交易记录消息绑定
*/
@Bean
public Binding jcppTransactionBinding(Queue jcppTransactionQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppTransactionQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_TRANSACTION);
}
/**
* 枪状态消息绑定
*/
@Bean
public Binding jcppGunStatusBinding(Queue jcppGunStatusQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppGunStatusQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_GUN_STATUS);
}
/**
* 计费模板消息绑定(通配符)
*/
@Bean
public Binding jcppPricingBinding(Queue jcppPricingQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppPricingQueue).to(jcppUplinkExchange).with("jcpp.uplink.pricing.#");
}
/**
* 远程操作结果消息绑定(通配符)
*/
@Bean
public Binding jcppRemoteResultBinding(Queue jcppRemoteResultQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppRemoteResultQueue).to(jcppUplinkExchange).with("jcpp.uplink.remoteResult.#");
}
/**
* 会话关闭消息绑定
*/
@Bean
public Binding jcppSessionCloseBinding(Queue jcppSessionCloseQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppSessionCloseQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_SESSION_CLOSE);
}
}

View File

@@ -0,0 +1,395 @@
package com.jsowell.pile.jcpp.constant;
/**
* JCPP 常量类
*
* @author jsowell
*/
public class JcppConstants {
private JcppConstants() {
}
// ==================== RabbitMQ Exchange ====================
/**
* 上行消息 Exchange
*/
public static final String UPLINK_EXCHANGE = "jcpp.uplink.exchange";
// ==================== RabbitMQ Queues ====================
/**
* 登录消息队列
*/
public static final String QUEUE_LOGIN = "jcpp.login.queue";
/**
* 心跳消息队列
*/
public static final String QUEUE_HEARTBEAT = "jcpp.heartbeat.queue";
/**
* 启动充电消息队列
*/
public static final String QUEUE_START_CHARGE = "jcpp.startCharge.queue";
/**
* 实时数据消息队列
*/
public static final String QUEUE_REAL_TIME_DATA = "jcpp.realTimeData.queue";
/**
* 交易记录消息队列
*/
public static final String QUEUE_TRANSACTION = "jcpp.transaction.queue";
/**
* 枪状态消息队列
*/
public static final String QUEUE_GUN_STATUS = "jcpp.gunStatus.queue";
/**
* 计费模板消息队列
*/
public static final String QUEUE_PRICING = "jcpp.pricing.queue";
/**
* 远程操作结果消息队列
*/
public static final String QUEUE_REMOTE_RESULT = "jcpp.remoteResult.queue";
/**
* 会话关闭消息队列
*/
public static final String QUEUE_SESSION_CLOSE = "jcpp.sessionClose.queue";
// ==================== RabbitMQ Routing Keys ====================
/**
* 登录消息路由键
*/
public static final String ROUTING_KEY_LOGIN = "jcpp.uplink.login";
/**
* 心跳消息路由键
*/
public static final String ROUTING_KEY_HEARTBEAT = "jcpp.uplink.heartbeat";
/**
* 启动充电消息路由键
*/
public static final String ROUTING_KEY_START_CHARGE = "jcpp.uplink.startCharge";
/**
* 实时数据消息路由键
*/
public static final String ROUTING_KEY_REAL_TIME_DATA = "jcpp.uplink.realTimeData";
/**
* 交易记录消息路由键
*/
public static final String ROUTING_KEY_TRANSACTION = "jcpp.uplink.transaction";
/**
* 枪状态消息路由键
*/
public static final String ROUTING_KEY_GUN_STATUS = "jcpp.uplink.gunStatus";
/**
* 会话关闭消息路由键
*/
public static final String ROUTING_KEY_SESSION_CLOSE = "jcpp.uplink.sessionClose";
// ==================== Redis Key 前缀 ====================
/**
* JCPP 下行指令 Redis Key 前缀
* 完整 key: jcpp:downlink:{pileCode}
*/
public static final String REDIS_DOWNLINK_PREFIX = "jcpp:downlink:";
/**
* JCPP 会话信息 Redis Key 前缀
* 完整 key: jcpp:session:{pileCode}
*/
public static final String REDIS_SESSION_PREFIX = "jcpp:session:";
/**
* 在线充电桩集合 Redis Key
*/
public static final String REDIS_ONLINE_PILES_KEY = "jcpp:online:piles";
/**
* JCPP 会话信息 Redis Key 前缀(兼容旧版本)
* 完整 key: jcpp:session:{pileCode}
*/
public static final String REDIS_KEY_SESSION = "jcpp:session:";
/**
* JCPP 节点信息 Redis Key 前缀
* 完整 key: jcpp:node:{pileCode}
*/
public static final String REDIS_KEY_NODE = "jcpp:node:";
/**
* 充电桩在线状态 Redis Key 前缀
* 完整 key: jcpp:online:{pileCode}
*/
public static final String REDIS_KEY_ONLINE = "jcpp:online:";
/**
* 会话过期时间(秒)- 默认5分钟
*/
public static final long SESSION_EXPIRE_SECONDS = 300L;
/**
* 在线状态过期时间(秒)- 默认3分钟
*/
public static final long ONLINE_EXPIRE_SECONDS = 180L;
// ==================== 消息类型枚举 ====================
/**
* 上行消息类型
*/
public static class MessageType {
/**
* 充电桩登录
*/
public static final String LOGIN = "LOGIN";
/**
* 心跳
*/
public static final String HEARTBEAT = "HEARTBEAT";
/**
* 刷卡/扫码启动充电
*/
public static final String START_CHARGE = "START_CHARGE";
/**
* 实时数据上报
*/
public static final String REAL_TIME_DATA = "REAL_TIME_DATA";
/**
* 交易记录(充电结束)
*/
public static final String TRANSACTION_RECORD = "TRANSACTION_RECORD";
/**
* 枪状态变化
*/
public static final String GUN_STATUS = "GUN_STATUS";
/**
* 校验计费模板
*/
public static final String VERIFY_PRICING = "VERIFY_PRICING";
/**
* 查询计费模板
*/
public static final String QUERY_PRICING = "QUERY_PRICING";
/**
* 远程启动结果
*/
public static final String REMOTE_START_RESULT = "REMOTE_START_RESULT";
/**
* 远程停止结果
*/
public static final String REMOTE_STOP_RESULT = "REMOTE_STOP_RESULT";
/**
* 会话关闭
*/
public static final String SESSION_CLOSE = "SESSION_CLOSE";
private MessageType() {
}
}
// ==================== 下行指令类型 ====================
/**
* 下行指令类型
*/
public static class DownlinkCommand {
/**
* 登录应答
*/
public static final String LOGIN_ACK = "LOGIN_ACK";
/**
* 远程启动充电
*/
public static final String REMOTE_START = "REMOTE_START";
/**
* 远程停止充电
*/
public static final String REMOTE_STOP = "REMOTE_STOP";
/**
* 下发计费模板
*/
public static final String SET_PRICING = "SET_PRICING";
/**
* 查询计费应答
*/
public static final String QUERY_PRICING_ACK = "QUERY_PRICING_ACK";
/**
* 校验计费应答
*/
public static final String VERIFY_PRICING_ACK = "VERIFY_PRICING_ACK";
/**
* 启动充电应答
*/
public static final String START_CHARGE_ACK = "START_CHARGE_ACK";
/**
* 交易记录应答
*/
public static final String TRANSACTION_RECORD_ACK = "TRANSACTION_RECORD_ACK";
private DownlinkCommand() {
}
}
// ==================== 启动类型 ====================
/**
* 充电启动类型
*/
public static class StartType {
/**
* 刷卡启动
*/
public static final String CARD = "CARD";
/**
* APP/小程序启动
*/
public static final String APP = "APP";
/**
* VIN码启动
*/
public static final String VIN = "VIN";
private StartType() {
}
}
// ==================== 鉴权失败原因 ====================
/**
* 鉴权失败原因
*/
public static class AuthFailReason {
/**
* 账户不存在
*/
public static final String ACCOUNT_NOT_EXISTS = "ACCOUNT_NOT_EXISTS";
/**
* 账户冻结
*/
public static final String ACCOUNT_FROZEN = "ACCOUNT_FROZEN";
/**
* 余额不足
*/
public static final String INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE";
/**
* 密码错误
*/
public static final String PASSWORD_ERROR = "PASSWORD_ERROR";
/**
* 充电桩停用
*/
public static final String PILE_DISABLED = "PILE_DISABLED";
/**
* 充电枪故障
*/
public static final String GUN_FAULT = "GUN_FAULT";
/**
* 充电枪占用
*/
public static final String GUN_OCCUPIED = "GUN_OCCUPIED";
/**
* 系统错误
*/
public static final String SYSTEM_ERROR = "SYSTEM_ERROR";
private AuthFailReason() {
}
}
// ==================== 停止原因 ====================
/**
* 充电停止原因
*/
public static class StopReason {
/**
* 用户主动停止
*/
public static final String USER_STOP = "USER_STOP";
/**
* 充满自停
*/
public static final String FULL_STOP = "FULL_STOP";
/**
* 金额用尽
*/
public static final String BALANCE_EXHAUSTED = "BALANCE_EXHAUSTED";
/**
* 异常停止
*/
public static final String ABNORMAL_STOP = "ABNORMAL_STOP";
/**
* 远程停止
*/
public static final String REMOTE_STOP = "REMOTE_STOP";
private StopReason() {
}
}
// ==================== 计费类型 ====================
/**
* 计费明细类型
*/
public static class PricingDetailType {
/**
* 峰谷计费
*/
public static final String PEAK_VALLEY = "PEAK_VALLEY";
/**
* 时段计费
*/
public static final String TIME_PERIOD = "TIME_PERIOD";
private PricingDetailType() {
}
}
}

View File

@@ -0,0 +1,100 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 枪状态消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppGunStatusConsumer {
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
/**
* 枪状态映射JCPP 状态 -> 系统状态
* JCPP: IDLE, INSERTED, CHARGING, CHARGE_COMPLETE, FAULT, UNKNOWN
* 系统: 0-离网, 1-空闲, 2-占用(未充电), 3-占用(充电中), 4-占用(预约锁定), 255-故障
*/
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"; // 离网
}
}
@RabbitListener(queues = JcppConstants.QUEUE_GUN_STATUS)
public void handleGunStatus(String message) {
try {
log.info("收到 JCPP 枪状态消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("枪状态消息格式错误: {}", message);
return;
}
// 从 data 中获取信息
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
String gunRunStatus = data.getString("gunRunStatus");
JSONArray faultMessages = data.getJSONArray("faultMessages");
if (pileCode == null || gunNo == null) {
log.warn("枪状态消息缺少必要字段: {}", message);
return;
}
// 构建枪口编码pileCode + gunNo
String pileConnectorCode = pileCode + gunNo;
// 映射状态
String status = mapGunStatus(gunRunStatus);
// 更新枪状态
int result = pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, status);
if (result > 0) {
log.info("更新枪状态成功: pileConnectorCode={}, status={}", pileConnectorCode, status);
} else {
log.warn("更新枪状态失败: pileConnectorCode={}, status={}", pileConnectorCode, status);
}
// 记录故障信息
if (faultMessages != null && !faultMessages.isEmpty()) {
log.warn("充电枪故障: pileConnectorCode={}, faults={}", pileConnectorCode, faultMessages);
// TODO: 可以将故障信息保存到数据库或发送告警
}
} catch (Exception e) {
log.error("处理 JCPP 枪状态消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,59 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* JCPP 心跳消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppHeartbeatConsumer {
private static final String HEARTBEAT_KEY_PREFIX = "jcpp:heartbeat:";
private static final long HEARTBEAT_EXPIRE_SECONDS = 180L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = JcppConstants.QUEUE_HEARTBEAT)
public void handleHeartbeat(String message) {
try {
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.debug("心跳消息格式错误: {}", message);
return;
}
// 从 data 中获取 pileCode
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
if (pileCode == null || pileCode.isEmpty()) {
log.debug("心跳消息缺少 pileCode: {}", message);
return;
}
// 更新最后活跃时间到 Redis避免频繁写数据库
String key = HEARTBEAT_KEY_PREFIX + pileCode;
stringRedisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),
HEARTBEAT_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.debug("收到充电桩心跳: pileCode={}", pileCode);
} catch (Exception e) {
log.error("处理 JCPP 心跳消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,72 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.PileBasicInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 登录消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppLoginConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@RabbitListener(queues = JcppConstants.QUEUE_LOGIN)
public void handleLogin(String message) {
try {
log.info("收到 JCPP 登录消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("登录消息格式错误: {}", message);
return;
}
// 从 data 中获取 pileCode
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
if (pileCode == null || pileCode.isEmpty()) {
log.warn("登录消息缺少 pileCode: {}", message);
return;
}
// 查询充电桩是否存在
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
boolean exists = pileInfo != null;
if (exists) {
// 更新充电桩在线状态
// PileBasicInfo updateInfo = new PileBasicInfo();
// updateInfo.setId(pileInfo.getId());
// updateInfo.setOnlineStatus("1"); // 1-在线
// pileBasicInfoService.updatePileBasicInfo(updateInfo);
log.info("充电桩登录成功: pileCode={}", pileCode);
} else {
log.warn("充电桩不存在: pileCode={}", pileCode);
}
// 发送登录应答
jcppDownlinkService.sendLoginAck(pileCode, exists);
} catch (Exception e) {
log.error("处理 JCPP 登录消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,147 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.PileBillingTemplate;
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.util.PricingModelConverter;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileBillingTemplateService;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 计费查询消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppPricingConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@RabbitListener(queues = JcppConstants.QUEUE_PRICING)
public void handlePricing(String message) {
try {
log.info("收到 JCPP 计费消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("计费消息格式错误: {}", message);
return;
}
String messageType = uplinkMessage.getMessageType();
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
if (pileCode == null || pileCode.isEmpty()) {
log.warn("计费消息缺少 pileCode: {}", message);
return;
}
// 根据消息类型处理
if (JcppConstants.MessageType.QUERY_PRICING.equals(messageType)) {
handleQueryPricing(pileCode);
} else if (JcppConstants.MessageType.VERIFY_PRICING.equals(messageType)) {
Long pricingId = data.getLong("pricingId");
handleVerifyPricing(pileCode, pricingId);
} else {
log.warn("未知的计费消息类型: {}", messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 计费消息异常: message={}", message, e);
}
}
/**
* 处理查询计费模板请求
*/
private void handleQueryPricing(String pileCode) {
try {
// 根据 pileCode 查询充电桩
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
log.warn("充电桩不存在: pileCode={}", pileCode);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
// 2. 获取关联的计费模板 ID
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());
// 获取充电桩关联的计费模板 ID
// Long billingTemplateId = pileInfo.getBillingTemplateId();
// if (billingTemplateId == null) {
// log.warn("充电桩未配置计费模板: pileCode={}", pileCode);
// jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
// return;
// }
// 查询计费模板
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(String pileCode, Long 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);
}
}
}

View File

@@ -0,0 +1,107 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
/**
* JCPP 实时数据消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppRealTimeDataConsumer {
private static final String REALTIME_DATA_KEY_PREFIX = "jcpp:realtime:";
private static final long REALTIME_DATA_EXPIRE_SECONDS = 300L;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = JcppConstants.QUEUE_REAL_TIME_DATA)
public void handleRealTimeData(String message) {
try {
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.debug("实时数据消息格式错误: {}", message);
return;
}
// 从 data 中获取信息
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
String outputVoltage = data.getString("outputVoltage");
String outputCurrent = data.getString("outputCurrent");
Integer soc = data.getInteger("soc");
Integer totalChargingDurationMin = data.getInteger("totalChargingDurationMin");
String totalChargingEnergyKWh = data.getString("totalChargingEnergyKWh");
String totalChargingCostYuan = data.getString("totalChargingCostYuan");
if (tradeNo == null || tradeNo.isEmpty()) {
log.debug("实时数据消息缺少 tradeNo: {}", message);
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) {
log.debug("订单不存在: tradeNo={}", tradeNo);
return;
}
// 检查订单状态是否为充电中
if (order.getOrderStatus() != null && StringUtils.equals(order.getOrderStatus(), "1")) {
// 更新订单实时数据
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
if (totalChargingEnergyKWh != null) {
// updateOrder.setTotalElectricity(new BigDecimal(totalChargingEnergyKWh));
}
if (totalChargingCostYuan != null) {
updateOrder.setOrderAmount(new BigDecimal(totalChargingCostYuan));
}
if (totalChargingDurationMin != null) {
// updateOrder.setChargingDuration(totalChargingDurationMin);
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
}
// 更新枪状态为充电中
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
log.debug("处理实时数据: tradeNo={}, soc={}, energy={}kWh", tradeNo, soc, totalChargingEnergyKWh);
} catch (Exception e) {
log.error("处理 JCPP 实时数据消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,126 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* JCPP 远程操作结果消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppRemoteResultConsumer {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_REMOTE_RESULT)
public void handleRemoteResult(String message) {
try {
log.info("收到 JCPP 远程操作结果消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("远程操作结果消息格式错误: {}", message);
return;
}
String messageType = uplinkMessage.getMessageType();
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
// 根据消息类型处理
if (JcppConstants.MessageType.REMOTE_START_RESULT.equals(messageType)) {
handleRemoteStartResult(data);
} else if (JcppConstants.MessageType.REMOTE_STOP_RESULT.equals(messageType)) {
handleRemoteStopResult(data);
} else {
log.warn("未知的远程操作结果消息类型: {}", messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 远程操作结果消息异常: message={}", message, e);
}
}
/**
* 处理远程启动结果
*/
private void handleRemoteStartResult(JSONObject data) {
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("远程启动结果缺少 tradeNo");
return;
}
// 根据 tradeNo 查询订单
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) {
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
if (Boolean.TRUE.equals(success)) {
log.info("远程停止成功: pileCode={}, gunNo={}", pileCode, gunNo);
} else {
log.warn("远程停止失败: pileCode={}, gunNo={}, reason=", pileCode, gunNo, failReason);
}
// 等待交易记录消息进行最终结算
}
}

View File

@@ -0,0 +1,76 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 会话关闭消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppSessionCloseConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@RabbitListener(queues = JcppConstants.QUEUE_SESSION_CLOSE)
public void handleSessionClose(String message) {
try {
log.info("收到 JCPP 会话关闭消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("会话关闭消息格式错误: {}", message);
return;
}
// 从 data 中获取信息
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
String reason = data.getString("reason");
if (pileCode == null || pileCode.isEmpty()) {
log.warn("会话关闭消息缺少 pileCode: {}", message);
return;
}
log.info("充电桩会话关闭: pileCode={}, reason={}", pileCode, reason);
// 1. 更新充电桩离线状态
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo != null) {
PileBasicInfo updateInfo = new PileBasicInfo();
updateInfo.setId(pileInfo.getId());
// updateInfo.setOnlineStatus(0); // 0-离线
pileBasicInfoService.updatePileBasicInfo(updateInfo);
log.info("更新充电桩离线状态: pileCode={}", pileCode);
}
// 2. 更新所有枪状态为离线
int result = pileConnectorInfoService.updateConnectorStatusByPileSn(pileCode, "0");
log.info("更新枪状态为离线: pileCode={}, affectedRows={}", pileCode, result);
// 3. TODO: 查询是否有正在充电的订单,如果有则标记为异常
// 这里需要根据实际业务逻辑处理正在充电的订单
// 可以调用 OrderBasicInfoService 查询并处理
} catch (Exception e) {
log.error("处理 JCPP 会话关闭消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,247 @@
package com.jsowell.pile.jcpp.consumer;
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.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
/**
* JCPP 刷卡启动充电消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppStartChargeConsumer {
@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 IJcppDownlinkService jcppDownlinkService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_START_CHARGE)
public void handleStartCharge(String message) {
try {
log.info("收到 JCPP 启动充电消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("启动充电消息格式错误: {}", message);
return;
}
// 从 data 中获取信息
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
String startType = data.getString("startType");
String cardNo = data.getString("cardNo");
Boolean needPassword = data.getBoolean("needPassword");
String password = data.getString("password");
if (pileCode == null || gunNo == null) {
log.warn("启动充电消息缺少必要字段: {}", message);
return;
}
// 处理刷卡启动
if ("CARD".equals(startType)) {
handleCardStartCharge(pileCode, gunNo, cardNo, needPassword, password);
} else {
log.warn("不支持的启动类型: {}", startType);
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, null, cardNo, null,
false, "不支持的启动类型");
}
} catch (Exception e) {
log.error("处理 JCPP 启动充电消息异常: message={}", message, e);
}
}
/**
* 处理刷卡启动充电
*/
private void handleCardStartCharge(String pileCode, String gunNo, String cardNo,
Boolean needPassword, String password) {
String failReason = null;
String tradeNo = null;
String limitYuan = null;
boolean authSuccess = false;
try {
// 1. 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
failReason = "充电桩不存在";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查充电桩状态
if (pileInfo.getDelFlag() != null && StringUtils.equals(pileInfo.getDelFlag(), "1")) {
failReason = "充电桩已停用";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 2. 查询授权卡信息
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
if (authCard == null) {
failReason = "账户不存在";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查卡状态
if (authCard.getStatus() != null && !StringUtils.equals(authCard.getStatus(), "1")) {
failReason = "账户已冻结";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 3. 如果需要密码验证
// if (Boolean.TRUE.equals(needPassword)) {
// if (password == null || !password.equals(authCard.getPassword())) {
// failReason = "密码错误";
// sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
// return;
// }
// }
// 4. 查询会员信息和钱包余额
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());
}
}
}
// 5. 检查白名单
boolean isWhitelist = checkWhitelist(pileInfo.getStationId(), cardNo, memberId);
// 6. 验证余额(非白名单用户需要检查余额)
BigDecimal minAmount = new BigDecimal("1.00"); // 最低充电金额
if (!isWhitelist && balance.compareTo(minAmount) < 0) {
failReason = "余额不足";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 7. 生成交易流水号
tradeNo = IdUtils.fastSimpleUUID();
limitYuan = balance.toString();
// 8. 创建充电订单
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 = "系统错误";
}
// 发送鉴权结果
sendAck(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);
if (pileStationWhitelist == null) {
return false;
}
return true;
// for (PileStationWhitelist whitelist : whitelists) {
// // 检查卡号
// if (cardNo != null && cardNo.equals(whitelist.getLogicCardNo())) {
// return true;
// }
// // 检查会员ID
// if (memberId != null && memberId.equals(whitelist.getMemberId())) {
// return true;
// }
// }
} catch (Exception e) {
log.error("检查白名单异常: stationId={}", stationId, e);
}
return false;
}
/**
* 发送启动充电应答
*/
private void sendAck(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);
}
}
}

View File

@@ -0,0 +1,128 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
/**
* JCPP 交易记录消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppTransactionConsumer {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_TRANSACTION)
public void handleTransaction(String message) {
try {
log.info("收到 JCPP 交易记录消息: {}", message);
// 解析消息
JcppUplinkMessage uplinkMessage = JSON.parseObject(message, JcppUplinkMessage.class);
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("交易记录消息格式错误: {}", message);
return;
}
// 从 data 中获取信息
JSONObject data = JSON.parseObject(JSON.toJSONString(uplinkMessage.getData()));
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
Long startTs = data.getLong("startTs");
Long endTs = data.getLong("endTs");
String totalEnergyKWh = data.getString("totalEnergyKWh");
String totalAmountYuan = data.getString("totalAmountYuan");
String stopReason = data.getString("stopReason");
JSONObject detail = data.getJSONObject("detail");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("交易记录消息缺少 tradeNo: {}", message);
return;
}
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, false);
return;
}
// 幂等性检查:避免重复处理
if (order.getOrderStatus() != null && StringUtils.equals(order.getOrderStatus(), "2")) {
log.info("订单已处理,跳过: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
return;
}
// 更新订单信息
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("2"); // 充电完成
if (startTs != null) {
updateOrder.setChargeStartTime(new Date(startTs));
}
if (endTs != null) {
updateOrder.setChargeEndTime(new Date(endTs));
}
if (totalEnergyKWh != null) {
// updateOrder.setTotalElectricity(new BigDecimal(totalEnergyKWh));
}
if (totalAmountYuan != null) {
updateOrder.setOrderAmount(new BigDecimal(totalAmountYuan));
}
if (stopReason != null) {
updateOrder.setReason(stopReason);
}
// 保存充电明细数据
if (detail != null) {
// updateOrder.setChargeDetail(detail.toJSONString());
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
log.info("更新订单完成: tradeNo={}, totalEnergy={}kWh, totalAmount={}元",
tradeNo, totalEnergyKWh, totalAmountYuan);
// 更新枪状态为空闲
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "1");
// 发送交易记录应答
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
// TODO: 触发结算流程
// orderBasicInfoService.realTimeOrderSplit(order.getId());
} catch (Exception e) {
log.error("处理 JCPP 交易记录消息异常: message={}", message, e);
}
}
}

View File

@@ -0,0 +1,48 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 下行指令通用结构
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppDownlinkCommand implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 指令ID
*/
private String commandId;
/**
* 指令类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.DownlinkCommand
*/
private String commandType;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 时间戳
*/
private Long timestamp;
/**
* 指令数据
*/
private Object data;
}

View File

@@ -0,0 +1,53 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 下行请求
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppDownlinkRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private String sessionId;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 指令类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.DownlinkCommand
*/
private String commandType;
/**
* 指令数据
*/
private Object data;
/**
* 请求ID用于追踪
*/
private String requestId;
/**
* 时间戳
*/
private Long timestamp;
}

View File

@@ -0,0 +1,63 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* JCPP 登录消息数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppLoginData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 认证凭证
*/
private String credential;
/**
* 远程地址
*/
private String remoteAddress;
/**
* JCPP 节点ID
*/
private String nodeId;
/**
* JCPP 节点主机地址
*/
private String nodeHostAddress;
/**
* JCPP 节点 REST 端口
*/
private Integer nodeRestPort;
/**
* JCPP 节点 gRPC 端口
*/
private Integer nodeGrpcPort;
/**
* 附加信息
*/
private Map<String, Object> additionalInfo;
}

View File

@@ -0,0 +1,180 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* JCPP 计费模板
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppPricingModel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 计费模板ID
*/
private Long pricingId;
/**
* 计费模板名称
*/
private String pricingName;
/**
* 计费类型1-标准计费 2-峰谷计费 3-时段计费
*/
private Integer pricingType;
/**
* 电费单价(标准计费时使用)
*/
private BigDecimal electricityPrice;
/**
* 服务费单价(标准计费时使用)
*/
private BigDecimal servicePrice;
/**
* 时段计费明细
*/
private List<TimePeriodPrice> timePeriodPrices;
/**
* 峰谷计费明细
*/
private PeakValleyPrice peakValleyPrice;
/**
* 时段计费明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodPrice implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间格式HH:mm
*/
private String startTime;
/**
* 结束时间格式HH:mm
*/
private String endTime;
/**
* 电费单价
*/
private BigDecimal electricityPrice;
/**
* 服务费单价
*/
private BigDecimal servicePrice;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
/**
* 峰谷计费明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PeakValleyPrice implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 尖时电费
*/
private BigDecimal sharpElectricityPrice;
/**
* 尖时服务费
*/
private BigDecimal sharpServicePrice;
/**
* 峰时电费
*/
private BigDecimal peakElectricityPrice;
/**
* 峰时服务费
*/
private BigDecimal peakServicePrice;
/**
* 平时电费
*/
private BigDecimal flatElectricityPrice;
/**
* 平时服务费
*/
private BigDecimal flatServicePrice;
/**
* 谷时电费
*/
private BigDecimal valleyElectricityPrice;
/**
* 谷时服务费
*/
private BigDecimal valleyServicePrice;
/**
* 时段配置
*/
private List<TimePeriodConfig> timePeriodConfigs;
}
/**
* 时段配置
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间格式HH:mm
*/
private String startTime;
/**
* 结束时间格式HH:mm
*/
private String endTime;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
}

View File

@@ -0,0 +1,82 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 实时数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppRealTimeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 输出电压V
*/
private String outputVoltage;
/**
* 输出电流A
*/
private String outputCurrent;
/**
* SOC 百分比
*/
private Integer soc;
/**
* 充电时长(分钟)
*/
private Integer totalChargingDurationMin;
/**
* 充电电量kWh
*/
private String totalChargingEnergyKWh;
/**
* 充电费用(元)
*/
private String totalChargingCostYuan;
/**
* 剩余充电时间(分钟)
*/
private Integer remainingTimeMin;
/**
* 枪状态
*/
private String gunStatus;
/**
* 充电功率kW
*/
private String chargingPowerKW;
}

View File

@@ -0,0 +1,47 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 远程启动结果数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppRemoteStartResultData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 是否成功
*/
private Boolean success;
/**
* 失败原因
*/
private String failReason;
}

View File

@@ -0,0 +1,77 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 会话信息
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppSessionInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private String sessionId;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 远程地址
*/
private String remoteAddress;
/**
* JCPP 节点ID
*/
private String nodeId;
/**
* JCPP 节点主机地址
*/
private String nodeHostAddress;
/**
* JCPP 节点 REST 端口
*/
private Integer nodeRestPort;
/**
* JCPP 节点 gRPC 端口
*/
private Integer nodeGrpcPort;
/**
* 协议名称
*/
private String protocolName;
/**
* 登录时间戳
*/
private Long loginTimestamp;
/**
* 最后活跃时间戳
*/
private Long lastActiveTimestamp;
/**
* 是否在线
*/
private Boolean online;
}

View File

@@ -0,0 +1,62 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 刷卡/扫码启动充电数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppStartChargeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 启动类型CARD-刷卡, APP-APP/小程序, VIN-VIN码
*/
private String startType;
/**
* 卡号或账号(逻辑卡号)
*/
private String cardNo;
/**
* 物理卡号
*/
private String physicalCardNo;
/**
* 是否需要密码
*/
private Boolean needPassword;
/**
* 密码
*/
private String password;
/**
* 车辆VIN码
*/
private String carVinCode;
}

View File

@@ -0,0 +1,190 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* JCPP 交易记录数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppTransactionData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 开始时间戳
*/
private Long startTs;
/**
* 结束时间戳
*/
private Long endTs;
/**
* 总电量kWh
*/
private String totalEnergyKWh;
/**
* 总金额(元)- 可选,如果充电桩不上报则由平台计算
*/
private String totalAmountYuan;
/**
* 交易时间戳
*/
private Long tradeTs;
/**
* 停止原因
*/
private String stopReason;
/**
* 电量明细
*/
private EnergyDetail detail;
/**
* 电量明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class EnergyDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 明细类型PEAK_VALLEY-峰谷计费, TIME_PERIOD-时段计费
*/
private String type;
/**
* 峰谷明细
*/
private PeakValleyDetail peakValley;
/**
* 时段明细列表
*/
private List<TimePeriodDetail> timePeriods;
}
/**
* 峰谷明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PeakValleyDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 尖时电量kWh
*/
private BigDecimal sharpEnergyKWh;
/**
* 尖时金额(元)
*/
private BigDecimal sharpAmountYuan;
/**
* 峰时电量kWh
*/
private BigDecimal peakEnergyKWh;
/**
* 峰时金额(元)
*/
private BigDecimal peakAmountYuan;
/**
* 平时电量kWh
*/
private BigDecimal flatEnergyKWh;
/**
* 平时金额(元)
*/
private BigDecimal flatAmountYuan;
/**
* 谷时电量kWh
*/
private BigDecimal valleyEnergyKWh;
/**
* 谷时金额(元)
*/
private BigDecimal valleyAmountYuan;
}
/**
* 时段明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间HH:mm
*/
private String startTime;
/**
* 结束时间HH:mm
*/
private String endTime;
/**
* 电量kWh
*/
private BigDecimal energyKWh;
/**
* 金额(元)
*/
private BigDecimal amountYuan;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
}

View File

@@ -0,0 +1,58 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 上行消息
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppUplinkMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息ID
*/
private String messageId;
/**
* 会话ID
*/
private String sessionId;
/**
* 协议名称
*/
private String protocolName;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 消息类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.MessageType
*/
private String messageType;
/**
* 时间戳
*/
private Long timestamp;
/**
* 具体消息内容(根据 messageType 不同,结构不同)
*/
private Object data;
}

View File

@@ -0,0 +1,96 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 上行消息响应
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppUplinkResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
/**
* 响应数据
*/
private Object data;
/**
* 消息ID原样返回
*/
private String messageId;
/**
* 成功响应
*/
public static JcppUplinkResponse success() {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.build();
}
/**
* 成功响应(带数据)
*/
public static JcppUplinkResponse success(Object data) {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.data(data)
.build();
}
/**
* 成功响应带消息ID
*/
public static JcppUplinkResponse success(String messageId, Object data) {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.messageId(messageId)
.data(data)
.build();
}
/**
* 失败响应
*/
public static JcppUplinkResponse error(String message) {
return JcppUplinkResponse.builder()
.success(false)
.message(message)
.build();
}
/**
* 失败响应带消息ID
*/
public static JcppUplinkResponse error(String messageId, String message) {
return JcppUplinkResponse.builder()
.success(false)
.message(message)
.messageId(messageId)
.build();
}
}

View File

@@ -0,0 +1,173 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import java.util.List;
import java.util.Map;
/**
* JCPP 下行调用服务接口
*
* @author jsowell
*/
public interface IJcppDownlinkService {
/**
* 发送登录应答
*
* @param pileCode 充电桩编码
* @param success 是否登录成功
*/
void sendLoginAck(String pileCode, boolean success);
/**
* 发送远程启动充电指令
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param tradeNo 交易流水号
* @param limitYuan 限制金额(元)
* @param logicalCardNo 逻辑卡号
* @param physicalCardNo 物理卡号
*/
void sendRemoteStartCharging(String pileCode, String gunNo, String tradeNo,
String limitYuan, String logicalCardNo, String physicalCardNo);
/**
* 发送远程停止充电指令
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
*/
void sendRemoteStopCharging(String pileCode, String gunNo);
/**
* 发送计费模板
*
* @param pileCode 充电桩编码
* @param pricingId 计费模板ID
* @param pricingModel 计费模板
*/
void sendSetPricing(String pileCode, Long pricingId, Object pricingModel);
/**
* 发送查询计费应答
*
* @param pileCode 充电桩编码
* @param pricingId 计费模板ID
* @param pricingModel 计费模板
*/
void sendQueryPricingAck(String pileCode, Long pricingId, Object pricingModel);
/**
* 发送校验计费应答
*
* @param pileCode 充电桩编码
* @param success 是否校验成功
* @param pricingId 计费模板ID
*/
void sendVerifyPricingAck(String pileCode, boolean success, Long pricingId);
/**
* 发送启动充电应答(刷卡鉴权结果)
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param tradeNo 交易流水号
* @param logicalCardNo 逻辑卡号
* @param limitYuan 限制金额(元)
* @param authSuccess 鉴权是否成功
* @param failReason 失败原因
*/
void sendStartChargeAck(String pileCode, String gunNo, String tradeNo,
String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason);
/**
* 发送交易记录应答
*
* @param pileCode 充电桩编码
* @param tradeNo 交易流水号
* @param success 是否成功
*/
void sendTransactionRecordAck(String pileCode, String tradeNo, boolean success);
/**
* 检查充电桩是否在线
*
* @param pileCode 充电桩编码
* @return 是否在线
*/
boolean isPileOnline(String pileCode);
/**
* 获取充电桩会话信息
*
* @param pileCode 充电桩编码
* @return 会话信息
*/
Map<String, String> getSessionInfo(String pileCode);
// ==================== 兼容旧接口 ====================
/**
* 远程启动充电(兼容旧接口)
*/
boolean remoteStartCharging(String sessionId, String pileCode, String gunNo,
String tradeNo, String limitYuan,
String logicalCardNo, String physicalCardNo);
/**
* 远程停止充电(兼容旧接口)
*/
boolean remoteStopCharging(String sessionId, String pileCode, String gunNo);
/**
* 下发计费模板(兼容旧接口)
*/
boolean setPricing(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel);
/**
* 查询计费应答(兼容旧接口)
*/
boolean queryPricingAck(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel);
/**
* 校验计费应答(兼容旧接口)
*/
boolean verifyPricingAck(String sessionId, String pileCode, boolean success, Long pricingId);
/**
* 登录应答(兼容旧接口)
*/
boolean loginAck(String sessionId, String pileCode, boolean success);
/**
* 启动充电应答(兼容旧接口)
*/
boolean startChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason);
/**
* 交易记录应答(兼容旧接口)
*/
boolean transactionRecordAck(String sessionId, String tradeNo, boolean success);
/**
* 查询充电桩会话信息(兼容旧接口)
*/
JcppSessionInfo getSession(String pileCode);
/**
* 查询所有在线充电桩(兼容旧接口)
*/
List<JcppSessionInfo> getSessions();
/**
* 发送通用下行指令(兼容旧接口)
*/
Map<String, Object> sendDownlinkCommand(String sessionId, String pileCode,
String commandType, Object data);
}

View File

@@ -0,0 +1,100 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.dto.JcppUplinkResponse;
/**
* JCPP 消息处理服务接口
*
* @author jsowell
*/
public interface IJcppMessageService {
/**
* 处理上行消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleMessage(JcppUplinkMessage message);
/**
* 处理登录消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleLogin(JcppUplinkMessage message);
/**
* 处理心跳消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleHeartbeat(JcppUplinkMessage message);
/**
* 处理刷卡/扫码启动充电消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleStartCharge(JcppUplinkMessage message);
/**
* 处理实时数据上报消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRealTimeData(JcppUplinkMessage message);
/**
* 处理交易记录消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleTransactionRecord(JcppUplinkMessage message);
/**
* 处理枪状态变化消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleGunStatus(JcppUplinkMessage message);
/**
* 处理校验计费模板消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleVerifyPricing(JcppUplinkMessage message);
/**
* 处理查询计费模板消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleQueryPricing(JcppUplinkMessage message);
/**
* 处理远程启动结果消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRemoteStartResult(JcppUplinkMessage message);
/**
* 处理远程停止结果消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRemoteStopResult(JcppUplinkMessage message);
}

View File

@@ -0,0 +1,46 @@
package com.jsowell.pile.jcpp.service;
/**
* JCPP 远程充电服务接口
* 用于 APP/小程序发起的远程启动/停止充电
*
* @author jsowell
*/
public interface IJcppRemoteChargeService {
/**
* 远程启动充电
*
* @param memberId 会员ID
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param payAmount 预付金额(元)
* @return 订单号
*/
String remoteStartCharging(String memberId, String pileCode, String gunNo, String payAmount);
/**
* 远程停止充电
*
* @param memberId 会员ID
* @param orderCode 订单号
* @return 是否成功
*/
boolean remoteStopCharging(String memberId, String orderCode);
/**
* 检查充电桩是否在线
*
* @param pileCode 充电桩编码
* @return 是否在线
*/
boolean isPileOnline(String pileCode);
/**
* 获取充电桩会话ID
*
* @param pileCode 充电桩编码
* @return 会话ID
*/
String getSessionId(String pileCode);
}

View File

@@ -0,0 +1,432 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.jsowell.pile.jcpp.config.JcppConfig;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppDownlinkCommand;
import com.jsowell.pile.jcpp.dto.JcppDownlinkRequest;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* JCPP 下行调用服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppDownlinkServiceImpl implements IJcppDownlinkService {
@Autowired
private JcppConfig jcppConfig;
@Autowired
@Qualifier("jcppRestTemplate")
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// ==================== 新接口实现(基于 Redis 队列) ====================
@Override
public void sendLoginAck(String pileCode, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
sendCommand(pileCode, JcppConstants.DownlinkCommand.LOGIN_ACK, data);
}
@Override
public void sendRemoteStartCharging(String pileCode, String gunNo, String tradeNo,
String limitYuan, String logicalCardNo, String physicalCardNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("limitYuan", limitYuan);
data.put("logicalCardNo", logicalCardNo);
data.put("physicalCardNo", physicalCardNo);
sendCommand(pileCode, JcppConstants.DownlinkCommand.REMOTE_START, data);
}
@Override
public void sendRemoteStopCharging(String pileCode, String gunNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
sendCommand(pileCode, JcppConstants.DownlinkCommand.REMOTE_STOP, data);
}
@Override
public void sendSetPricing(String pileCode, Long pricingId, Object pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
sendCommand(pileCode, JcppConstants.DownlinkCommand.SET_PRICING, data);
}
@Override
public void sendQueryPricingAck(String pileCode, Long pricingId, Object pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
sendCommand(pileCode, JcppConstants.DownlinkCommand.QUERY_PRICING_ACK, data);
}
@Override
public void sendVerifyPricingAck(String pileCode, boolean success, Long pricingId) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
data.put("pricingId", pricingId);
sendCommand(pileCode, JcppConstants.DownlinkCommand.VERIFY_PRICING_ACK, data);
}
@Override
public void sendStartChargeAck(String pileCode, String gunNo, String tradeNo,
String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("logicalCardNo", logicalCardNo);
data.put("limitYuan", limitYuan);
data.put("authSuccess", authSuccess);
data.put("failReason", failReason);
sendCommand(pileCode, JcppConstants.DownlinkCommand.START_CHARGE_ACK, data);
}
@Override
public void sendTransactionRecordAck(String pileCode, String tradeNo, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("tradeNo", tradeNo);
data.put("success", success);
sendCommand(pileCode, JcppConstants.DownlinkCommand.TRANSACTION_RECORD_ACK, data);
}
@Override
public boolean isPileOnline(String pileCode) {
Boolean isMember = stringRedisTemplate.opsForSet().isMember(JcppConstants.REDIS_ONLINE_PILES_KEY, pileCode);
return Boolean.TRUE.equals(isMember);
}
@Override
public Map<String, String> getSessionInfo(String pileCode) {
String key = JcppConstants.REDIS_SESSION_PREFIX + pileCode;
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
Map<String, String> result = new HashMap<>();
entries.forEach((k, v) -> result.put(String.valueOf(k), String.valueOf(v)));
return result;
}
/**
* 发送下行指令到 Redis 队列
*/
private void sendCommand(String pileCode, String commandType, Object data) {
JcppDownlinkCommand command = JcppDownlinkCommand.builder()
.commandId(UUID.randomUUID().toString())
.commandType(commandType)
.pileCode(pileCode)
.timestamp(System.currentTimeMillis())
.data(data)
.build();
String key = JcppConstants.REDIS_DOWNLINK_PREFIX + pileCode;
String json = JSON.toJSONString(command);
stringRedisTemplate.opsForList().rightPush(key, json);
log.info("发送 JCPP 下行指令: pileCode={}, commandType={}, commandId={}",
pileCode, commandType, command.getCommandId());
}
// ==================== 兼容旧接口实现(基于 HTTP ====================
@Override
public boolean remoteStartCharging(String sessionId, String pileCode, String gunNo,
String tradeNo, String limitYuan,
String logicalCardNo, String physicalCardNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("limitYuan", limitYuan);
data.put("logicalCardNo", logicalCardNo);
data.put("physicalCardNo", physicalCardNo);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.REMOTE_START, data);
return isSuccess(result);
}
@Override
public boolean remoteStopCharging(String sessionId, String pileCode, String gunNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.REMOTE_STOP, data);
return isSuccess(result);
}
@Override
public boolean setPricing(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.SET_PRICING, data);
return isSuccess(result);
}
@Override
public boolean queryPricingAck(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.QUERY_PRICING_ACK, data);
return isSuccess(result);
}
@Override
public boolean verifyPricingAck(String sessionId, String pileCode, boolean success, Long pricingId) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
data.put("pricingId", pricingId);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.VERIFY_PRICING_ACK, data);
return isSuccess(result);
}
@Override
public boolean loginAck(String sessionId, String pileCode, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.LOGIN_ACK, data);
return isSuccess(result);
}
@Override
public boolean startChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("logicalCardNo", logicalCardNo);
data.put("limitYuan", limitYuan);
data.put("authSuccess", authSuccess);
data.put("failReason", failReason);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.START_CHARGE_ACK, data);
return isSuccess(result);
}
@Override
public boolean transactionRecordAck(String sessionId, String tradeNo, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("tradeNo", tradeNo);
data.put("success", success);
// 从 Redis 获取 pileCode
String pileCode = getPileCodeByTradeNo(tradeNo);
if (pileCode == null) {
log.warn("无法获取交易流水号对应的充电桩编码, tradeNo: {}", tradeNo);
return false;
}
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.TRANSACTION_RECORD_ACK, data);
return isSuccess(result);
}
@Override
public JcppSessionInfo getSession(String pileCode) {
// 先从 Redis 缓存获取
String cacheKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (sessionJson != null) {
return JSON.parseObject(sessionJson, JcppSessionInfo.class);
}
// 从 JCPP 服务获取
try {
String url = jcppConfig.getSessionUrl() + "/" + pileCode;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
if (Boolean.TRUE.equals(result.get("success"))) {
Object data = result.get("data");
if (data != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(JSON.toJSONString(data), JcppSessionInfo.class);
// 缓存到 Redis
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
return sessionInfo;
}
}
}
} catch (RestClientException e) {
log.error("获取 JCPP 会话信息失败, pileCode: {}, error: {}", pileCode, e.getMessage());
}
return null;
}
@Override
public List<JcppSessionInfo> getSessions() {
try {
String url = jcppConfig.getSessionUrl();
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
if (Boolean.TRUE.equals(result.get("success"))) {
Object data = result.get("data");
if (data != null) {
return JSON.parseArray(JSON.toJSONString(data), JcppSessionInfo.class);
}
}
}
} catch (RestClientException e) {
log.error("获取 JCPP 所有会话信息失败, error: {}", e.getMessage());
}
return Collections.emptyList();
}
@Override
public Map<String, Object> sendDownlinkCommand(String sessionId, String pileCode,
String commandType, Object data) {
if (!jcppConfig.isEnabled()) {
log.warn("JCPP 对接未启用,忽略下行指令: commandType={}, pileCode={}", commandType, pileCode);
return createErrorResult("JCPP 对接未启用");
}
// 如果没有传入 sessionId尝试从 Redis 获取
if (sessionId == null || sessionId.isEmpty()) {
sessionId = getSessionIdFromRedis(pileCode);
if (sessionId == null) {
log.warn("无法获取充电桩会话ID, pileCode: {}", pileCode);
return createErrorResult("充电桩不在线");
}
}
JcppDownlinkRequest request = JcppDownlinkRequest.builder()
.sessionId(sessionId)
.pileCode(pileCode)
.commandType(commandType)
.data(data)
.requestId(UUID.randomUUID().toString())
.timestamp(System.currentTimeMillis())
.build();
return sendWithRetry(request);
}
/**
* 带重试的发送
*/
private Map<String, Object> sendWithRetry(JcppDownlinkRequest request) {
int retryCount = jcppConfig.getRetryCount();
int retryInterval = jcppConfig.getRetryInterval();
for (int i = 0; i <= retryCount; i++) {
try {
return doSend(request);
} catch (RestClientException e) {
log.warn("发送 JCPP 下行指令失败, 第 {} 次尝试, requestId: {}, error: {}",
i + 1, request.getRequestId(), e.getMessage());
if (i < retryCount) {
try {
Thread.sleep(retryInterval);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
return createErrorResult("发送下行指令失败,已重试 " + retryCount + "");
}
/**
* 实际发送
*/
private Map<String, Object> doSend(JcppDownlinkRequest request) {
String url = jcppConfig.getDownlinkUrl();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<JcppDownlinkRequest> entity = new HttpEntity<>(request, headers);
log.info("发送 JCPP 下行指令: url=, requestId={}, commandType={}, pileCode={}",
url, request.getRequestId(), request.getCommandType(), request.getPileCode());
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
log.info("JCPP 下行指令响应: requestId={}, result={}", request.getRequestId(), result);
return result;
}
return createErrorResult("HTTP 响应异常: " + response.getStatusCode());
}
/**
* 从 Redis 获取会话ID
*/
private String getSessionIdFromRedis(String pileCode) {
String cacheKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
return sessionInfo.getSessionId();
}
return null;
}
/**
* 根据交易流水号获取充电桩编码
*/
private String getPileCodeByTradeNo(String tradeNo) {
// TODO: 从订单表或 Redis 中获取
return null;
}
/**
* 判断结果是否成功
*/
private boolean isSuccess(Map<String, Object> result) {
return result != null && Boolean.TRUE.equals(result.get("success"));
}
/**
* 创建错误结果
*/
private Map<String, Object> createErrorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", message);
return result;
}
}

View File

@@ -0,0 +1,627 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
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.*;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.service.IJcppMessageService;
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 消息处理服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppMessageServiceImpl implements IJcppMessageService {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private PileAuthCardService pileAuthCardService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public JcppUplinkResponse handleMessage(JcppUplinkMessage message) {
if (message == null || message.getMessageType() == null) {
log.warn("收到无效的 JCPP 消息: {}", message);
return JcppUplinkResponse.error("无效的消息");
}
String messageType = message.getMessageType();
log.info("收到 JCPP 上行消息, messageId: {}, messageType: {}, pileCode: {}",
message.getMessageId(), messageType, message.getPileCode());
try {
switch (messageType) {
case JcppConstants.MessageType.LOGIN:
return handleLogin(message);
case JcppConstants.MessageType.HEARTBEAT:
return handleHeartbeat(message);
case JcppConstants.MessageType.START_CHARGE:
return handleStartCharge(message);
case JcppConstants.MessageType.REAL_TIME_DATA:
return handleRealTimeData(message);
case JcppConstants.MessageType.TRANSACTION_RECORD:
return handleTransactionRecord(message);
case JcppConstants.MessageType.GUN_STATUS:
return handleGunStatus(message);
case JcppConstants.MessageType.VERIFY_PRICING:
return handleVerifyPricing(message);
case JcppConstants.MessageType.QUERY_PRICING:
return handleQueryPricing(message);
case JcppConstants.MessageType.REMOTE_START_RESULT:
return handleRemoteStartResult(message);
case JcppConstants.MessageType.REMOTE_STOP_RESULT:
return handleRemoteStopResult(message);
default:
log.warn("未知的消息类型: {}", messageType);
return JcppUplinkResponse.error(message.getMessageId(), "未知的消息类型: " + messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 消息异常, messageId: {}, messageType: {}, error: {}",
message.getMessageId(), messageType, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理消息异常: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleLogin(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理登录消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析登录数据
JcppLoginData loginData = parseData(message.getData(), JcppLoginData.class);
if (loginData == null) {
log.warn("登录消息数据解析失败, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "登录数据解析失败");
}
// 查询充电桩是否存在
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
log.warn("充电桩不存在, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩不存在");
}
// 检查充电桩是否被删除
if ("1".equals(pileInfo.getDelFlag())) {
log.warn("充电桩已被删除, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩已被删除");
}
// 保存会话信息到 Redis
JcppSessionInfo sessionInfo = JcppSessionInfo.builder()
.sessionId(sessionId)
.pileCode(pileCode)
.remoteAddress(loginData.getRemoteAddress())
.nodeId(loginData.getNodeId())
.nodeHostAddress(loginData.getNodeHostAddress())
.nodeRestPort(loginData.getNodeRestPort())
.nodeGrpcPort(loginData.getNodeGrpcPort())
.protocolName(message.getProtocolName())
.loginTimestamp(System.currentTimeMillis())
.lastActiveTimestamp(System.currentTimeMillis())
.online(true)
.build();
// 保存会话到 Redis
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
stringRedisTemplate.opsForValue().set(sessionKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 保存节点信息到 Redis
String nodeKey = JcppConstants.REDIS_KEY_NODE + pileCode;
stringRedisTemplate.opsForValue().set(nodeKey, JSON.toJSONString(loginData),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 设置在线状态
String onlineKey = JcppConstants.REDIS_KEY_ONLINE + pileCode;
stringRedisTemplate.opsForValue().set(onlineKey, "1",
JcppConstants.ONLINE_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.info("充电桩登录成功, pileCode: {}, sessionId: {}, remoteAddress: {}",
pileCode, sessionId, loginData.getRemoteAddress());
// 发送登录应答
jcppDownlinkService.loginAck(sessionId, pileCode, true);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
public JcppUplinkResponse handleHeartbeat(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.debug("处理心跳消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 更新会话过期时间
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(sessionKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
sessionInfo.setLastActiveTimestamp(System.currentTimeMillis());
stringRedisTemplate.opsForValue().set(sessionKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
// 更新节点信息过期时间
String nodeKey = JcppConstants.REDIS_KEY_NODE + pileCode;
stringRedisTemplate.expire(nodeKey, JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 更新在线状态过期时间
String onlineKey = JcppConstants.REDIS_KEY_ONLINE + pileCode;
stringRedisTemplate.opsForValue().set(onlineKey, "1",
JcppConstants.ONLINE_EXPIRE_SECONDS, TimeUnit.SECONDS);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleStartCharge(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理启动充电消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析启动充电数据
JcppStartChargeData startData = parseData(message.getData(), JcppStartChargeData.class);
if (startData == null) {
log.warn("启动充电数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "启动充电数据解析失败");
}
String gunNo = startData.getGunNo();
String cardNo = startData.getCardNo();
String startType = startData.getStartType();
try {
// 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.PILE_DISABLED);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩不存在");
}
// 根据启动类型进行鉴权
String memberId = null;
String merchantId = String.valueOf(pileInfo.getMerchantId());
BigDecimal balance = BigDecimal.ZERO;
if (JcppConstants.StartType.CARD.equals(startType)) {
// 刷卡启动 - 根据逻辑卡号查询授权卡
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
if (authCard == null) {
log.warn("授权卡不存在, cardNo: {}", cardNo);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_NOT_EXISTS);
return JcppUplinkResponse.error(message.getMessageId(), "授权卡不存在");
}
// 检查卡状态
if (!"1".equals(authCard.getStatus())) {
log.warn("授权卡已停用, cardNo: {}", cardNo);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_FROZEN);
return JcppUplinkResponse.error(message.getMessageId(), "授权卡已停用");
}
memberId = authCard.getMemberId();
} else if (JcppConstants.StartType.VIN.equals(startType)) {
// VIN码启动
String vinCode = startData.getCarVinCode();
// TODO: 根据VIN码查询会员信息
log.info("VIN码启动, vinCode: ", vinCode);
}
// 查询会员信息和余额
if (memberId != null) {
MemberBasicInfo memberInfo = memberBasicInfoService.selectInfoByMemberId(memberId);
if (memberInfo == null) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_NOT_EXISTS);
return JcppUplinkResponse.error(message.getMessageId(), "会员不存在");
}
// 检查会员状态
if ("1".equals(memberInfo.getStatus())) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_FROZEN);
return JcppUplinkResponse.error(message.getMessageId(), "会员账户已冻结");
}
// 查询钱包余额
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, merchantId);
if (walletInfo != null) {
BigDecimal principalBalance = walletInfo.getPrincipalBalance() != null ? walletInfo.getPrincipalBalance() : BigDecimal.ZERO;
BigDecimal giftBalance = walletInfo.getGiftBalance() != null ? walletInfo.getGiftBalance() : BigDecimal.ZERO;
balance = principalBalance.add(giftBalance);
}
// 检查余额是否充足最低1元
if (balance.compareTo(BigDecimal.ONE) < 0) {
log.warn("余额不足, memberId: {}, balance: {}", memberId, balance);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.INSUFFICIENT_BALANCE);
return JcppUplinkResponse.error(message.getMessageId(), "余额不足");
}
}
// 生成交易流水号
String tradeNo = IdUtils.fastSimpleUUID();
// 创建订单
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setTransactionCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + "-" + gunNo);
order.setMemberId(memberId);
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setOrderStatus("1"); // 充电中
order.setPayStatus("0"); // 待支付
order.setChargeStartTime(new Date());
order.setCreateTime(new Date());
orderBasicInfoService.insert(order);
// 保存交易流水号与充电桩的映射关系到 Redis
String tradeKey = "jcpp:trade:" + tradeNo;
stringRedisTemplate.opsForValue().set(tradeKey, pileCode, 24, TimeUnit.HOURS);
// 发送启动充电应答
sendStartChargeAck(sessionId, pileCode, gunNo, tradeNo, cardNo, balance.toString(), true, null);
log.info("刷卡启动充电鉴权成功, pileCode: {}, gunNo: {}, tradeNo: {}, memberId: {}, balance: {}",
pileCode, gunNo, tradeNo, memberId, balance);
return JcppUplinkResponse.success(message.getMessageId(), tradeNo);
} catch (Exception e) {
log.error("处理启动充电消息异常, pileCode: {}, error: {}", pileCode, e.getMessage(), e);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.SYSTEM_ERROR);
return JcppUplinkResponse.error(message.getMessageId(), "处理启动充电异常: " + e.getMessage());
}
}
/**
* 发送启动充电应答
*/
private void sendStartChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String cardNo, String limitYuan,
boolean success, String failReason) {
try {
jcppDownlinkService.startChargeAck(sessionId, pileCode, gunNo, tradeNo, cardNo, limitYuan, success, failReason);
} catch (Exception e) {
log.error("发送启动充电应答失败, pileCode: {}, error: {}", pileCode, e.getMessage());
}
}
@Override
public JcppUplinkResponse handleRealTimeData(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.debug("处理实时数据消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// 解析实时数据
JcppRealTimeData realTimeData = parseData(message.getData(), JcppRealTimeData.class);
if (realTimeData == null) {
log.warn("实时数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "实时数据解析失败");
}
String tradeNo = realTimeData.getTradeNo();
String gunNo = realTimeData.getGunNo();
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
// 更新订单实时数据
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
if (realTimeData.getTotalChargingEnergyKWh() != null) {
// updateOrder.setTotalElectricity(new BigDecimal(realTimeData.getTotalChargingEnergyKWh()));
}
if (realTimeData.getTotalChargingCostYuan() != null) {
// updateOrder.setTotalAmount(new BigDecimal(realTimeData.getTotalChargingCostYuan()));
}
if (realTimeData.getTotalChargingDurationMin() != null) {
// updateOrder.setTotalTime(realTimeData.getTotalChargingDurationMin());
}
updateOrder.setUpdateTime(new Date());
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
// 保存实时数据到 Redis用于实时查询
String realtimeKey = "jcpp:realtime:" + tradeNo;
stringRedisTemplate.opsForValue().set(realtimeKey, JSON.toJSONString(realTimeData), 10, TimeUnit.MINUTES);
// 保存监控数据到数据库(可选,根据需要控制写入频率)
// saveMonitorData(order, realTimeData);
log.debug("实时数据处理成功, tradeNo: {}, energy: {}kWh, cost: {}元",
tradeNo, realTimeData.getTotalChargingEnergyKWh(), realTimeData.getTotalChargingCostYuan());
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理实时数据异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理实时数据异常: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleTransactionRecord(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理交易记录消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析交易记录数据
JcppTransactionData transactionData = parseData(message.getData(), JcppTransactionData.class);
if (transactionData == null) {
log.warn("交易记录数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "交易记录数据解析失败");
}
String tradeNo = transactionData.getTradeNo();
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
// 仍然发送确认,避免充电桩重复上报
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
// 幂等性检查:如果订单已经是完成状态,直接返回成功
if ("2".equals(order.getOrderStatus())) {
log.info("订单已完成,忽略重复的交易记录, tradeNo: {}", tradeNo);
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
// 更新订单信息
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("2"); // 充电完成
updateOrder.setChargeEndTime(transactionData.getEndTs() != null ? new Date(transactionData.getEndTs()) : new Date());
updateOrder.setReason(transactionData.getStopReason());
// 设置电量
if (transactionData.getTotalEnergyKWh() != null) {
// updateOrder.setTotalElectricity(new BigDecimal(transactionData.getTotalEnergyKWh()));
}
// 设置金额(如果充电桩上报了金额则使用,否则需要根据计费模板计算)
if (transactionData.getTotalAmountYuan() != null) {
// updateOrder.setTotalAmount(new BigDecimal(transactionData.getTotalAmountYuan()));
}
// 计算充电时长
if (transactionData.getStartTs() != null && transactionData.getEndTs() != null) {
long durationMin = (transactionData.getEndTs() - transactionData.getStartTs()) / 60000;
// updateOrder.setTotalTime((int) durationMin);
}
updateOrder.setUpdateTime(new Date());
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
// 更新枪状态为空闲
String connectorCode = pileCode + "-" + transactionData.getGunNo();
// pileConnectorInfoService.updateConnectorStatus(connectorCode, "0"); // 空闲
// 发送交易记录确认
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
// 清理 Redis 中的实时数据
String realtimeKey = "jcpp:realtime:" + tradeNo;
stringRedisTemplate.delete(realtimeKey);
log.info("交易记录处理成功, tradeNo: {}, energy: {}kWh, amount: {}元, stopReason: {}",
tradeNo, transactionData.getTotalEnergyKWh(),
transactionData.getTotalAmountYuan(), transactionData.getStopReason());
// TODO: 触发结算流程(如果是预付费模式)
// orderBasicInfoService.realTimeOrderSplit(order);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理交易记录异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理交易记录异常: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleGunStatus(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.info("处理枪状态消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// TODO: 解析枪状态数据并更新数据库
// 枪状态变化通常用于更新充电枪的实时状态
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
public JcppUplinkResponse handleVerifyPricing(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理校验计费消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 校验计费模板充电桩上报当前使用的计费模板ID平台校验是否一致
// 如果不一致,需要重新下发计费模板
try {
// 获取平台当前的计费模板
BillingTemplateVO billingTemplate = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
Long currentPricingId = billingTemplate != null ? Long.parseLong(billingTemplate.getTemplateId()) : null;
// TODO: 从消息中获取充电桩上报的计费模板ID进行比对
// 这里简化处理,直接返回成功
jcppDownlinkService.verifyPricingAck(sessionId, pileCode, true, currentPricingId);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("校验计费模板失败, pileCode: , error: {}", pileCode, e.getMessage(), e);
jcppDownlinkService.verifyPricingAck(sessionId, pileCode, false, null);
return JcppUplinkResponse.error(message.getMessageId(), "校验计费模板失败: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleQueryPricing(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理查询计费消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
try {
// 根据充电桩编码查询计费模板
BillingTemplateVO billingTemplate = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
if (billingTemplate == null) {
log.warn("未找到充电桩的计费模板, pileCode: {}", pileCode);
jcppDownlinkService.queryPricingAck(sessionId, pileCode, null, null);
return JcppUplinkResponse.error(message.getMessageId(), "未找到计费模板");
}
// 转换为 JCPP 计费模板格式
JcppPricingModel pricingModel = PricingModelConverter.convert(billingTemplate);
// 发送计费模板应答
jcppDownlinkService.queryPricingAck(sessionId, pileCode, Long.parseLong(billingTemplate.getTemplateId()), pricingModel);
log.info("查询计费模板成功, pileCode: {}, templateId: {}, templateName: {}",
pileCode, billingTemplate.getTemplateId(), billingTemplate.getTemplateName());
return JcppUplinkResponse.success(message.getMessageId(), pricingModel);
} catch (Exception e) {
log.error("查询计费模板失败, pileCode: {}, error: {}", pileCode, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "查询计费模板失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleRemoteStartResult(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理远程启动结果消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析远程启动结果数据
JcppRemoteStartResultData resultData = parseData(message.getData(), JcppRemoteStartResultData.class);
if (resultData == null) {
log.warn("远程启动结果数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "远程启动结果数据解析失败");
}
String tradeNo = resultData.getTradeNo();
boolean success = Boolean.TRUE.equals(resultData.getSuccess());
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setUpdateTime(new Date());
if (success) {
// 启动成功
updateOrder.setOrderStatus("1"); // 充电中
log.info("远程启动充电成功, tradeNo: {}, pileCode: {}, gunNo: {}",
tradeNo, pileCode, resultData.getGunNo());
} else {
// 启动失败
updateOrder.setOrderStatus("4"); // 异常结束
updateOrder.setReason(resultData.getFailReason());
log.warn("远程启动充电失败, tradeNo: {}, pileCode: {}, failReason: {}",
tradeNo, pileCode, resultData.getFailReason());
// TODO: 触发退款流程(如果已预付)
// refundService.refund(order);
}
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理远程启动结果异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理远程启动结果异常: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleRemoteStopResult(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.info("处理远程停止结果消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// 远程停止结果通常不需要特殊处理,等待交易记录上报即可
// 这里只记录日志
return JcppUplinkResponse.success(message.getMessageId(), null);
}
/**
* 解析消息数据
*/
private <T> T parseData(Object data, Class<T> clazz) {
if (data == null) {
return null;
}
try {
if (data instanceof String) {
return JSON.parseObject((String) data, clazz);
}
return JSON.parseObject(JSON.toJSONString(data), clazz);
} catch (Exception e) {
log.error("解析消息数据失败, data: , clazz: {}, error: {}", data, clazz.getName(), e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,179 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.jsowell.common.util.id.IdUtils;
import com.jsowell.pile.domain.MemberBasicInfo;
import com.jsowell.pile.domain.MemberWalletInfo;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.service.IJcppRemoteChargeService;
import com.jsowell.pile.service.MemberBasicInfoService;
import com.jsowell.pile.service.MemberWalletInfoService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileBasicInfoService;
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 远程充电服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppRemoteChargeServiceImpl implements IJcppRemoteChargeService {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public String remoteStartCharging(String memberId, String pileCode, String gunNo, String payAmount) {
log.info("远程启动充电, memberId: {}, pileCode: {}, gunNo: {}, payAmount: {}",
memberId, pileCode, gunNo, payAmount);
// 1. 检查充电桩是否在线(使用新的 Redis Set 方式)
if (!jcppDownlinkService.isPileOnline(pileCode)) {
throw new RuntimeException("充电桩离线,请稍后重试");
}
// 2. 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
throw new RuntimeException("充电桩不存在");
}
// 3. 查询会员信息
MemberBasicInfo memberInfo = memberBasicInfoService.selectInfoByMemberId(memberId);
if (memberInfo == null) {
throw new RuntimeException("会员不存在");
}
// 4. 检查会员状态
if ("1".equals(memberInfo.getStatus())) {
throw new RuntimeException("会员账户已冻结");
}
// 5. 查询钱包余额
String merchantId = String.valueOf(pileInfo.getMerchantId());
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, merchantId);
BigDecimal balance = BigDecimal.ZERO;
if (walletInfo != null) {
BigDecimal principalBalance = walletInfo.getPrincipalBalance() != null ? walletInfo.getPrincipalBalance() : BigDecimal.ZERO;
BigDecimal giftBalance = walletInfo.getGiftBalance() != null ? walletInfo.getGiftBalance() : BigDecimal.ZERO;
balance = principalBalance.add(giftBalance);
}
// 6. 检查余额是否充足
BigDecimal payAmountDecimal = new BigDecimal(payAmount);
if (balance.compareTo(payAmountDecimal) < 0) {
throw new RuntimeException("余额不足");
}
// 7. 生成订单
String tradeNo = IdUtils.fastSimpleUUID();
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setTransactionCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + "-" + gunNo);
order.setMemberId(memberId);
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setOrderStatus("0"); // 待充电(等待启动结果)
order.setPayStatus("0"); // 待支付
order.setChargeStartTime(new Date());
order.setCreateTime(new Date());
order.setPayAmount(payAmountDecimal);
orderBasicInfoService.insert(order);
// 8. 保存交易流水号映射(用于后续查询)
String tradeKey = "jcpp:trade:" + tradeNo;
stringRedisTemplate.opsForValue().set(tradeKey, pileCode, 24, TimeUnit.HOURS);
// 9. 发送远程启动指令(使用新的 Redis 队列方式)
jcppDownlinkService.sendRemoteStartCharging(pileCode, gunNo, tradeNo, payAmount, null, null);
log.info("远程启动充电指令已发送, tradeNo: {}, pileCode: {}, gunNo: {}", tradeNo, pileCode, gunNo);
return tradeNo;
}
@Override
public boolean remoteStopCharging(String memberId, String orderCode) {
log.info("远程停止充电, memberId: {}, orderCode: {}", memberId, orderCode);
// 1. 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
if (order == null) {
throw new RuntimeException("订单不存在");
}
// 2. 验证会员
if (!memberId.equals(order.getMemberId())) {
throw new RuntimeException("无权操作此订单");
}
// 3. 检查订单状态
if (!"1".equals(order.getOrderStatus())) {
throw new RuntimeException("订单状态不允许停止充电");
}
String pileCode = order.getPileSn();
String gunNo = order.getConnectorCode().replace(pileCode + "-", "");
// 4. 检查充电桩是否在线
if (!jcppDownlinkService.isPileOnline(pileCode)) {
throw new RuntimeException("充电桩离线,请稍后重试");
}
// 5. 发送远程停止指令(使用新的 Redis 队列方式)
jcppDownlinkService.sendRemoteStopCharging(pileCode, gunNo);
log.info("远程停止充电指令已发送, orderCode: {}, pileCode: {}, gunNo: {}", orderCode, pileCode, gunNo);
return true;
}
@Override
public boolean isPileOnline(String pileCode) {
// 使用新的 Redis Set 方式检查在线状态
return jcppDownlinkService.isPileOnline(pileCode);
}
@Override
public String getSessionId(String pileCode) {
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(sessionKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
return sessionInfo.getSessionId();
}
return null;
}
}

View File

@@ -0,0 +1,274 @@
package com.jsowell.pile.jcpp.util;
import com.jsowell.pile.domain.PileBillingDetail;
import com.jsowell.pile.domain.PileBillingTemplate;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.vo.web.BillingDetailVO;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 计费模板转换工具类
*
* @author jsowell
*/
@Slf4j
public class PricingModelConverter {
private PricingModelConverter() {
}
/**
* 将系统计费模板转换为 JCPP 计费模板格式
*
* @param template 系统计费模板
* @return JCPP 计费模板
*/
public static JcppPricingModel convert(PileBillingTemplate template) {
if (template == null) {
return null;
}
JcppPricingModel model = new JcppPricingModel();
model.setPricingId(template.getId());
model.setPricingName(template.getName());
List<PileBillingDetail> details = template.getPileBillingDetailList();
if (details == null || details.isEmpty()) {
// 无详情,使用标准计费
model.setPricingType(1);
return model;
}
// 根据详情判断计费类型
// 如果只有一条记录且时段为全天,则为标准计费
// 否则为峰谷计费或时段计费
if (details.size() == 1 && isAllDay(details.get(0).getApplyTime())) {
model.setPricingType(1);
model.setElectricityPrice(details.get(0).getElectricityPrice());
model.setServicePrice(details.get(0).getServicePrice());
} else {
// 峰谷计费
model.setPricingType(2);
model.setPeakValleyPrice(convertToPeakValley(details));
model.setTimePeriodPrices(convertToTimePeriods(details));
}
return model;
}
/**
* 将 BillingTemplateVO 转换为 JCPP 计费模板格式
*
* @param vo 计费模板 VO
* @return JCPP 计费模板
*/
public static JcppPricingModel convert(BillingTemplateVO vo) {
if (vo == null) {
return null;
}
JcppPricingModel model = new JcppPricingModel();
model.setPricingId(Long.parseLong(vo.getTemplateId()));
model.setPricingName(vo.getTemplateName());
List<BillingDetailVO> details = vo.getBillingDetailList();
if (details == null || details.isEmpty()) {
model.setPricingType(1);
return model;
}
if (details.size() == 1
//&& isAllDay(details.get(0).getApplyTime())
) {
model.setPricingType(1);
model.setElectricityPrice(details.get(0).getElectricityPrice());
model.setServicePrice(details.get(0).getServicePrice());
} else {
model.setPricingType(2);
model.setPeakValleyPrice(convertToPeakValleyFromVO(details));
model.setTimePeriodPrices(convertToTimePeriodsFromVO(details));
}
return model;
}
/**
* 判断是否为全天时段
*/
private static boolean isAllDay(String applyTime) {
if (applyTime == null) {
return true;
}
return "00:00-24:00".equals(applyTime) || "00:00-23:59".equals(applyTime)
|| applyTime.isEmpty();
}
/**
* 转换为峰谷计费明细
*/
private static JcppPricingModel.PeakValleyPrice convertToPeakValley(List<PileBillingDetail> details) {
JcppPricingModel.PeakValleyPrice peakValley = new JcppPricingModel.PeakValleyPrice();
List<JcppPricingModel.TimePeriodConfig> configs = new ArrayList<>();
for (PileBillingDetail detail : details) {
String timeType = detail.getTimeType();
BigDecimal electricityPrice = detail.getElectricityPrice();
BigDecimal servicePrice = detail.getServicePrice();
// 根据时段类型设置价格
switch (timeType) {
case "1": // 尖时
peakValley.setSharpElectricityPrice(electricityPrice);
peakValley.setSharpServicePrice(servicePrice);
break;
case "2": // 峰时
peakValley.setPeakElectricityPrice(electricityPrice);
peakValley.setPeakServicePrice(servicePrice);
break;
case "3": // 平时
peakValley.setFlatElectricityPrice(electricityPrice);
peakValley.setFlatServicePrice(servicePrice);
break;
case "4": // 谷时
peakValley.setValleyElectricityPrice(electricityPrice);
peakValley.setValleyServicePrice(servicePrice);
break;
default:
log.warn("未知的时段类型: {}", timeType);
}
// 解析时段配置
String applyTime = detail.getApplyTime();
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
configs.add(JcppPricingModel.TimePeriodConfig.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.periodType(Integer.parseInt(timeType))
.build());
}
}
}
}
peakValley.setTimePeriodConfigs(configs);
return peakValley;
}
/**
* 转换为时段计费明细
*/
private static List<JcppPricingModel.TimePeriodPrice> convertToTimePeriods(List<PileBillingDetail> details) {
List<JcppPricingModel.TimePeriodPrice> prices = new ArrayList<>();
for (PileBillingDetail detail : details) {
String applyTime = detail.getApplyTime();
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
prices.add(JcppPricingModel.TimePeriodPrice.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.electricityPrice(detail.getElectricityPrice())
.servicePrice(detail.getServicePrice())
.periodType(Integer.parseInt(detail.getTimeType()))
.build());
}
}
}
}
return prices;
}
/**
* 从 VO 转换为峰谷计费明细
*/
private static JcppPricingModel.PeakValleyPrice convertToPeakValleyFromVO(List<BillingDetailVO> details) {
JcppPricingModel.PeakValleyPrice peakValley = new JcppPricingModel.PeakValleyPrice();
List<JcppPricingModel.TimePeriodConfig> configs = new ArrayList<>();
for (BillingDetailVO detail : details) {
String timeType = detail.getTimeType();
BigDecimal electricityPrice = detail.getElectricityPrice();
BigDecimal servicePrice = detail.getServicePrice();
switch (timeType) {
case "1":
peakValley.setSharpElectricityPrice(electricityPrice);
peakValley.setSharpServicePrice(servicePrice);
break;
case "2":
peakValley.setPeakElectricityPrice(electricityPrice);
peakValley.setPeakServicePrice(servicePrice);
break;
case "3":
peakValley.setFlatElectricityPrice(electricityPrice);
peakValley.setFlatServicePrice(servicePrice);
break;
case "4":
peakValley.setValleyElectricityPrice(electricityPrice);
peakValley.setValleyServicePrice(servicePrice);
break;
default:
log.warn("未知的时段类型: {}", timeType);
}
String applyTime = detail.getApplyTime().get(0);
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
configs.add(JcppPricingModel.TimePeriodConfig.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.periodType(Integer.parseInt(timeType))
.build());
}
}
}
}
peakValley.setTimePeriodConfigs(configs);
return peakValley;
}
/**
* 从 VO 转换为时段计费明细
*/
private static List<JcppPricingModel.TimePeriodPrice> convertToTimePeriodsFromVO(List<BillingDetailVO> details) {
List<JcppPricingModel.TimePeriodPrice> prices = new ArrayList<>();
for (BillingDetailVO detail : details) {
String applyTime = detail.getApplyTime().get(0);
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
prices.add(JcppPricingModel.TimePeriodPrice.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.electricityPrice(detail.getElectricityPrice())
.servicePrice(detail.getServicePrice())
.periodType(Integer.parseInt(detail.getTimeType()))
.build());
}
}
}
}
return prices;
}
}