mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-04-19 18:45:03 +08:00
对接jcpp
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()); // 默认标准计费
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 等待交易记录消息进行最终结算
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user