添加查询充电枪状态接口

This commit is contained in:
三丙
2025-09-27 18:04:00 +08:00
parent 7a03cc98a7
commit a1e0a09320
74 changed files with 1727 additions and 259 deletions

View File

@@ -6,8 +6,10 @@
*/
package sanbing.jcpp.app.adapter.controller;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import sanbing.jcpp.app.adapter.request.GunCreateRequest;
@@ -17,16 +19,22 @@ import sanbing.jcpp.app.adapter.response.ApiResponse;
import sanbing.jcpp.app.adapter.response.GunWithStatusResponse;
import sanbing.jcpp.app.adapter.response.PageResponse;
import sanbing.jcpp.app.dal.entity.Gun;
import sanbing.jcpp.app.data.kv.AttrKeyEnum;
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
import sanbing.jcpp.app.service.AttributeService;
import sanbing.jcpp.app.service.GunService;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/api/guns")
@RequiredArgsConstructor
@Slf4j
public class GunController extends BaseController {
private final GunService gunService;
private final AttributeService attributeService;
@PostMapping
public ResponseEntity<ApiResponse<Gun>> createGun(@Valid @RequestBody GunCreateRequest request) {
@@ -58,4 +66,50 @@ public class GunController extends BaseController {
PageResponse<GunWithStatusResponse> guns = gunService.queryGunsWithStatus(request);
return ResponseEntity.ok(ApiResponse.success("查询成功", guns));
}
/**
* 根据枪编号获取充电枪运行状态
*/
@GetMapping("/status/{gunCode}")
public ResponseEntity<ApiResponse<String>> getGunStatusByGunCode(@PathVariable String gunCode) {
try {
// 首先根据枪编号查找充电枪
Gun gun = gunService.findByGunCode(gunCode);
if (gun == null) {
return ResponseEntity.ok(ApiResponse.error("充电枪不存在", null));
}
// 通过AttributeService获取充电枪运行状态
ListenableFuture<Optional<AttributeKvEntry>> attributeFuture =
attributeService.find(gun.getId(), AttrKeyEnum.GUN_RUN_STATUS.getCode());
Optional<AttributeKvEntry> attributeResult = attributeFuture.get();
String status = null;
if (attributeResult.isPresent()) {
AttributeKvEntry entry = attributeResult.get();
status = entry.getStrValue().orElse(null);
}
return ResponseEntity.ok(ApiResponse.success("查询成功", status));
} catch (Exception e) {
return ResponseEntity.ok(ApiResponse.error("查询失败: " + e.getMessage(), null));
}
}
/**
* 根据充电枪编码获取充电枪详细信息
*/
@GetMapping("/code/{gunCode}")
public ResponseEntity<ApiResponse<GunWithStatusResponse>> getGunByCode(@PathVariable String gunCode) {
try {
GunWithStatusResponse gun = gunService.findGunWithStatusByCode(gunCode);
if (gun == null) {
return ResponseEntity.ok(ApiResponse.error("充电枪不存在", null));
}
return ResponseEntity.ok(ApiResponse.success("查询成功", gun));
} catch (Exception e) {
return ResponseEntity.ok(ApiResponse.error("查询失败: " + e.getMessage(), null));
}
}
}

View File

@@ -6,8 +6,10 @@
*/
package sanbing.jcpp.app.adapter.controller;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import sanbing.jcpp.app.adapter.request.PileCreateRequest;
@@ -18,18 +20,24 @@ import sanbing.jcpp.app.adapter.response.PageResponse;
import sanbing.jcpp.app.adapter.response.PileOptionResponse;
import sanbing.jcpp.app.adapter.response.PileWithStatusResponse;
import sanbing.jcpp.app.dal.entity.Pile;
import sanbing.jcpp.app.data.kv.AttrKeyEnum;
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
import sanbing.jcpp.app.exception.JCPPException;
import sanbing.jcpp.app.service.AttributeService;
import sanbing.jcpp.app.service.PileService;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/api/piles")
@RequiredArgsConstructor
@Slf4j
public class PileController extends BaseController {
private final PileService pileService;
private final AttributeService attributeService;
@PostMapping
public ResponseEntity<ApiResponse<Pile>> createPile(@Valid @RequestBody PileCreateRequest request) {
@@ -70,4 +78,34 @@ public class PileController extends BaseController {
List<PileOptionResponse> options = pileService.getPileOptions();
return ResponseEntity.ok(ApiResponse.success("查询成功", options));
}
/**
* 根据桩编号获取充电桩状态
*/
@GetMapping("/status/{pileCode}")
public ResponseEntity<ApiResponse<String>> getPileStatusByPileCode(@PathVariable String pileCode) {
try {
// 首先根据桩编号查找充电桩
Pile pile = pileService.findByPileCode(pileCode);
if (pile == null) {
return ResponseEntity.ok(ApiResponse.error("充电桩不存在", null));
}
// 通过AttributeService获取充电桩状态
ListenableFuture<Optional<AttributeKvEntry>> attributeFuture =
attributeService.find(pile.getId(), AttrKeyEnum.STATUS.getCode());
Optional<AttributeKvEntry> attributeResult = attributeFuture.get();
String status = null;
if (attributeResult.isPresent()) {
AttributeKvEntry entry = attributeResult.get();
status = entry.getStrValue().orElse(null);
}
return ResponseEntity.ok(ApiResponse.success("查询成功", status));
} catch (Exception e) {
return ResponseEntity.ok(ApiResponse.error("查询失败: " + e.getMessage(), null));
}
}
}

View File

@@ -0,0 +1,196 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.controller;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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;
import sanbing.jcpp.app.adapter.dto.*;
import sanbing.jcpp.app.adapter.request.RpcRequest;
import sanbing.jcpp.app.adapter.response.ApiResponse;
import sanbing.jcpp.app.service.PileProtocolService;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.proto.gen.DownlinkProto.*;
/**
* RPC控制器 - 通用化的充电桩下行指令接口
*
* @author 九筒
*/
@RestController
@RequestMapping("/api/rpc")
@RequiredArgsConstructor
@Slf4j
public class RpcController extends BaseController {
private final PileProtocolService pileProtocolService;
/**
* 单向RPC接口 - 不等待充电桩返回消息
*/
@PostMapping("/oneway")
public ResponseEntity<ApiResponse<String>> onewayRpc(@RequestBody RpcRequest request) {
try {
log.info("收到单向RPC请求: method={}, parameter={}", request.getMethod(), request.getParameter());
// 根据method调用对应的服务方法
executeRpcMethod(request.getMethod(), request.getParameter());
return ResponseEntity.ok(ApiResponse.success("指令发送成功", null));
} catch (Exception e) {
log.error("单向RPC调用失败: method={}, error={}", request.getMethod(), e.getMessage(), e);
return ResponseEntity.ok(ApiResponse.error("指令发送失败: " + e.getMessage(), null));
}
}
/**
* 双向RPC接口 - 等待充电桩返回消息(带超时)
* TODO: 实现双向RPC逻辑包括超时处理
*/
@PostMapping("/bidirectional")
public ResponseEntity<ApiResponse<String>> bidirectionalRpc(@RequestBody RpcRequest request) {
// TODO: 实现双向RPC需要等待充电桩响应包含超时时间参数
return ResponseEntity.ok(ApiResponse.error("双向RPC功能待实现", null));
}
/**
* 执行RPC方法
*/
private void executeRpcMethod(String method, JsonNode parameter) throws Exception {
switch (method) {
case "startCharge":
handleStartCharge(parameter);
break;
case "stopCharge":
handleStopCharge(parameter);
break;
case "restartPile":
handleRestartPile(parameter);
break;
case "setPricing":
handleSetPricing(parameter);
break;
case "setQrcode":
handleSetQrcode(parameter);
break;
case "otaRequest":
handleOtaRequest(parameter);
break;
case "offlineCardBalanceUpdate":
handleOfflineCardBalanceUpdate(parameter);
break;
case "offlineCardSync":
handleOfflineCardSync(parameter);
break;
case "offlineCardClear":
handleOfflineCardClear(parameter);
break;
case "offlineCardQuery":
handleOfflineCardQuery(parameter);
break;
case "timeSync":
handleTimeSync(parameter);
break;
default:
throw new IllegalArgumentException("不支持的RPC方法: " + method);
}
}
/**
* 处理启动充电指令
*/
private void handleStartCharge(JsonNode parameter) {
StartChargeDTO startChargeDto = JacksonUtil.fromJson(parameter, StartChargeDTO.class);
pileProtocolService.startCharge(startChargeDto);
}
/**
* 处理停止充电指令
*/
private void handleStopCharge(JsonNode parameter) {
StopChargeDTO stopChargeDto = JacksonUtil.fromJson(parameter, StopChargeDTO.class);
pileProtocolService.stopCharge(stopChargeDto);
}
/**
* 处理重启充电桩指令
*/
private void handleRestartPile(JsonNode parameter) {
RestartPileDTO restartPileDto = JacksonUtil.fromJson(parameter, RestartPileDTO.class);
pileProtocolService.restartPile(restartPileDto);
}
/**
* 处理设置计费策略指令
*/
private void handleSetPricing(JsonNode parameter) {
SetPricingDTO setPricingDto = JacksonUtil.fromJson(parameter, SetPricingDTO.class);
pileProtocolService.setPricing(setPricingDto);
}
/**
* 处理设置二维码指令
*/
private void handleSetQrcode(JsonNode parameter) {
SetQrcodeRequest setQrcodeRequest = JacksonUtil.fromJson(parameter, SetQrcodeRequest.class);
pileProtocolService.setQrcode(setQrcodeRequest);
}
/**
* 处理OTA升级指令
*/
private void handleOtaRequest(JsonNode parameter) {
OtaRequest otaRequest = JacksonUtil.fromJson(parameter, OtaRequest.class);
pileProtocolService.otaRequest(otaRequest);
}
/**
* 处理离线卡余额更新指令
*/
private void handleOfflineCardBalanceUpdate(JsonNode parameter) {
OfflineCardBalanceUpdateRequest request = JacksonUtil.fromJson(parameter, OfflineCardBalanceUpdateRequest.class);
pileProtocolService.offlineCardBalanceUpdateRequest(request);
}
/**
* 处理离线卡同步指令
*/
private void handleOfflineCardSync(JsonNode parameter) {
OfflineCardSyncRequest request = JacksonUtil.fromJson(parameter, OfflineCardSyncRequest.class);
pileProtocolService.offlineCardSyncRequest(request);
}
/**
* 处理离线卡清除指令
*/
private void handleOfflineCardClear(JsonNode parameter) {
OfflineCardClearRequest request = JacksonUtil.fromJson(parameter, OfflineCardClearRequest.class);
pileProtocolService.offlineCardClearRequest(request);
}
/**
* 处理离线卡查询指令
*/
private void handleOfflineCardQuery(JsonNode parameter) {
OfflineCardQueryRequest request = JacksonUtil.fromJson(parameter, OfflineCardQueryRequest.class);
pileProtocolService.offlineCardQueryRequest(request);
}
/**
* 处理时间同步指令
*/
private void handleTimeSync(JsonNode parameter) {
TimeSyncDTO timeSyncDto = JacksonUtil.fromJson(parameter, TimeSyncDTO.class);
pileProtocolService.timeSync(timeSyncDto);
}
}

View File

@@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sanbing.jcpp.app.adapter.dto.*;
import sanbing.jcpp.app.service.PileProtocolService;
import sanbing.jcpp.proto.gen.DownlinkProto.*;
@@ -39,8 +40,16 @@ public class TestController extends BaseController {
String logicalCardNo = RandomStringUtils.secure().nextNumeric(12);
String physicalCardNo = RandomStringUtils.secure().nextNumeric(12);
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("50"), orderNo,
logicalCardNo, physicalCardNo, null);
StartChargeDTO startChargeDto = new StartChargeDTO();
startChargeDto.setPileCode("20231212000010");
startChargeDto.setGunNo("01");
startChargeDto.setLimitYuan(new BigDecimal("50"));
startChargeDto.setOrderNo(orderNo);
startChargeDto.setLogicalCardNo(logicalCardNo);
startChargeDto.setPhysicalCardNo(physicalCardNo);
startChargeDto.setParallelNo(null);
pileProtocolService.startCharge(startChargeDto);
return ResponseEntity.ok("success");
}
@@ -53,8 +62,16 @@ public class TestController extends BaseController {
String physicalCardNo = RandomStringUtils.secure().nextNumeric(12);
String parallelNo = RandomStringUtils.secure().nextNumeric(6);
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("100"),
orderNo, logicalCardNo, physicalCardNo, parallelNo);
StartChargeDTO startChargeDto = new StartChargeDTO();
startChargeDto.setPileCode("20231212000010");
startChargeDto.setGunNo("01");
startChargeDto.setLimitYuan(new BigDecimal("100"));
startChargeDto.setOrderNo(orderNo);
startChargeDto.setLogicalCardNo(logicalCardNo);
startChargeDto.setPhysicalCardNo(physicalCardNo);
startChargeDto.setParallelNo(parallelNo);
pileProtocolService.startCharge(startChargeDto);
return ResponseEntity.ok("success");
}
@@ -62,7 +79,11 @@ public class TestController extends BaseController {
@GetMapping("/stopCharge")
public ResponseEntity<String> stopCharge() {
pileProtocolService.stopCharge("20231212000010", "01");
StopChargeDTO stopChargeDto = new StopChargeDTO();
stopChargeDto.setPileCode("20231212000010");
stopChargeDto.setGunNo("01");
pileProtocolService.stopCharge(stopChargeDto);
return ResponseEntity.ok("success");
}
@@ -85,7 +106,11 @@ public class TestController extends BaseController {
@GetMapping("/restartPile")
public ResponseEntity<String> restartPile() {
pileProtocolService.restartPile("20231212000010", 1);
RestartPileDTO restartPileDto = new RestartPileDTO();
restartPileDto.setPileCode("20231212000010");
restartPileDto.setType(1);
pileProtocolService.restartPile(restartPileDto);
return ResponseEntity.ok("success");
}
@@ -195,13 +220,16 @@ public class TestController extends BaseController {
.setPeakValleyPricing(peakValleyPricing) // 设置峰谷计价配置
.build();
pileProtocolService.setPricing(pileCode,
SetPricingRequest.newBuilder()
SetPricingDTO setPricingDto = new SetPricingDTO();
setPricingDto.setPileCode(pileCode);
setPricingDto.setSetPricingRequest(SetPricingRequest.newBuilder()
.setPileCode(pileCode)
.setPricingId(1000L)
.setPricingModel(pricingModel)
.build());
pileProtocolService.setPricing(setPricingDto);
return ResponseEntity.ok("success");
}
@@ -274,13 +302,16 @@ public class TestController extends BaseController {
.setTimePeriodPricing(timePeriodPricing) // 设置时段计价配置
.build();
pileProtocolService.setPricing(pileCode,
SetPricingRequest.newBuilder()
SetPricingDTO setPricingDto = new SetPricingDTO();
setPricingDto.setPileCode(pileCode);
setPricingDto.setSetPricingRequest(SetPricingRequest.newBuilder()
.setPileCode(pileCode)
.setPricingId(2000L)
.setPricingModel(pricingModel)
.build());
pileProtocolService.setPricing(setPricingDto);
return ResponseEntity.ok("Time period pricing test success");
}
@@ -309,7 +340,7 @@ public class TestController extends BaseController {
pileProtocolService.offlineCardBalanceUpdateRequest(OfflineCardBalanceUpdateRequest.newBuilder()
.setCardNo("1000000000123456")
.setPileCode("20231212000010")
.setGunCode("01")
.setGunNo("01")
.setLimitYuan("1000")
.build());
@@ -334,7 +365,11 @@ public class TestController extends BaseController {
@GetMapping("/timeSync")
public ResponseEntity<String> timeSync() {
pileProtocolService.timeSync("20231212000010", LocalDateTime.now());
TimeSyncDTO timeSyncDto = new TimeSyncDTO();
timeSyncDto.setPileCode("20231212000010");
timeSyncDto.setTime(LocalDateTime.now());
pileProtocolService.timeSync(timeSyncDto);
return ResponseEntity.ok("success");
}

View File

@@ -0,0 +1,32 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 重启充电桩DTO
*
* @author 九筒
*/
@Data
public class RestartPileDTO {
/**
* 充电桩编码
*/
@NotBlank(message = "充电桩编码不能为空")
private String pileCode;
/**
* 重启类型
*/
@NotNull(message = "重启类型不能为空")
private Integer type;
}

View File

@@ -0,0 +1,33 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import sanbing.jcpp.proto.gen.DownlinkProto.SetPricingRequest;
/**
* 设置计费策略DTO
*
* @author 九筒
*/
@Data
public class SetPricingDTO {
/**
* 充电桩编码
*/
@NotBlank(message = "充电桩编码不能为空")
private String pileCode;
/**
* 计费策略请求
*/
@NotNull(message = "计费策略请求不能为空")
private SetPricingRequest setPricingRequest;
}

View File

@@ -0,0 +1,61 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 启动充电DTO
*
* @author 九筒
*/
@Data
public class StartChargeDTO {
/**
* 充电桩编码
*/
@NotBlank(message = "充电桩编码不能为空")
private String pileCode;
/**
* 充电枪编号
*/
@NotBlank(message = "充电枪编号不能为空")
private String gunNo;
/**
* 限制金额(元)
*/
@NotNull(message = "限制金额不能为空")
private BigDecimal limitYuan;
/**
* 订单号
*/
@NotBlank(message = "订单号不能为空")
private String orderNo;
/**
* 逻辑卡号
*/
private String logicalCardNo;
/**
* 物理卡号
*/
private String physicalCardNo;
/**
* 并充序号(当不为空时,自动使用并充启机命令)
*/
private String parallelNo;
}

View File

@@ -0,0 +1,31 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 停止充电DTO
*
* @author 九筒
*/
@Data
public class StopChargeDTO {
/**
* 充电桩编码
*/
@NotBlank(message = "充电桩编码不能为空")
private String pileCode;
/**
* 充电枪编号
*/
@NotBlank(message = "充电枪编号不能为空")
private String gunNo;
}

View File

@@ -0,0 +1,34 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 时间同步DTO
*
* @author 九筒
*/
@Data
public class TimeSyncDTO {
/**
* 充电桩编码
*/
@NotBlank(message = "充电桩编码不能为空")
private String pileCode;
/**
* 同步时间
*/
@NotNull(message = "同步时间不能为空")
private LocalDateTime time;
}

View File

@@ -0,0 +1,51 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.adapter.request;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* RPC请求参数
*
* @author 九筒
*/
@Data
public class RpcRequest {
/**
* RPC方法名
* 支持的方法包括:
* - startCharge: 启动充电
* - stopCharge: 停止充电
* - restartPile: 重启充电桩
* - setPricing: 设置计费策略
* - setQrcode: 设置二维码
* - otaRequest: OTA升级
* - offlineCardBalanceUpdate: 离线卡余额更新
* - offlineCardSync: 离线卡同步
* - offlineCardClear: 离线卡清除
* - offlineCardQuery: 离线卡查询
* - timeSync: 时间同步
*/
@NotBlank(message = "方法名不能为空")
private String method;
/**
* 方法参数JSON格式
* 不同的方法需要不同的参数结构
*/
@NotNull(message = "参数不能为空")
private JsonNode parameter;
/**
* 超时时间毫秒仅用于双向RPC
*/
private Long timeoutMs;
}

View File

@@ -21,7 +21,8 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String errorCode;
private String message;
private T data;
@@ -29,6 +30,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.message("操作成功")
.data(data)
.timestamp(System.currentTimeMillis())
@@ -37,6 +39,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> success(String message, T data) {
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
@@ -45,6 +48,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> error(String errorCode, String message) {
return ApiResponse.<T>builder()
.success(false)
.errorCode(errorCode)
.message(message)
.timestamp(System.currentTimeMillis())
@@ -53,6 +57,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> error(ErrorCode errorCode, String message) {
return ApiResponse.<T>builder()
.success(false)
.errorCode(errorCode.getCode())
.message(message)
.timestamp(System.currentTimeMillis())
@@ -61,6 +66,7 @@ public class ApiResponse<T> {
public static <T> ApiResponse<T> error(ErrorCode errorCode) {
return ApiResponse.<T>builder()
.success(false)
.errorCode(errorCode.getCode())
.message(errorCode.getMessage())
.timestamp(System.currentTimeMillis())

View File

@@ -108,4 +108,9 @@ public class PileWithStatusResponse {
* 最后活跃时间13位时间戳
*/
private Long lastActiveTime;
/**
* 充电枪数量
*/
private Long gunCount;
}

View File

@@ -22,12 +22,15 @@ import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
@Configuration
@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class})
@EnableTransactionManagement
@MapperScan({"sanbing.jcpp.app.dal.mapper"})
public class DalConfig {
@@ -44,13 +47,11 @@ public class DalConfig {
return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Primary
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Primary
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
@@ -58,7 +59,12 @@ public class DalConfig {
@Primary
@Bean
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(@Qualifier("transactionManager") PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionTemplate.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
@@ -66,7 +72,6 @@ public class DalConfig {
}
@Bean
@Primary
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();

View File

@@ -20,11 +20,20 @@ import java.util.UUID;
* @author 九筒
*/
public interface GunMapper extends BaseMapper<Gun> {
/**
* 根据充电桩编码和充电枪编查询充电枪
* 根据充电桩编码和充电枪编查询充电枪
* 充电桩上报的是 pile_code + gun_no 的组合,这个组合是唯一的
*/
Gun selectByPileCodeAndGunCode(@Param("pileCode") String pileCode, @Param("gunCode") String gunCode);
Gun selectByPileCodeAndGunNo(@Param("pileCode") String pileCode, @Param("gunNo") String gunNo);
/**
* 根据枪编号查询充电枪
*/
Gun selectByGunCode(@Param("gunCode") String gunCode);
GunWithStatusResponse selectGunWithStatusByCode(@Param("gunCode") String gunCode);
/**
* 分页查询充电枪及其状态信息

View File

@@ -6,6 +6,7 @@
*/
package sanbing.jcpp.app.dal.repository;
import sanbing.jcpp.app.adapter.response.GunWithStatusResponse;
import sanbing.jcpp.app.dal.entity.Gun;
import java.util.UUID;
@@ -17,14 +18,26 @@ import java.util.UUID;
*/
public interface GunRepository {
/**
* 根据充电桩编码和充电枪编查询充电枪
* 根据充电桩编码和充电枪编查询充电枪
* 充电桩上报的是 pile_code + gun_no 的组合,这个组合是唯一的
*
* @param pileCode 充电桩编码
* @param gunNo 充电枪编号 (如: "01", "02")
* @return 充电枪实体如果不存在返回null
*/
Gun findByPileCodeAndGunNo(String pileCode, String gunNo);
/**
* 根据枪编号查询充电枪
*
* @param gunCode 充电枪编码
* @return 充电枪实体如果不存在返回null
*/
Gun findByPileCodeAndGunCode(String pileCode, String gunCode);
Gun findByGunCode(String gunCode);
GunWithStatusResponse findGunWithStatusByCode(String gunCode);
/**
* 根据充电枪ID查询充电枪

View File

@@ -6,10 +6,11 @@
*/
package sanbing.jcpp.app.dal.repository.impl;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.event.TransactionalEventListener;
import sanbing.jcpp.app.adapter.response.GunWithStatusResponse;
import sanbing.jcpp.app.dal.entity.Gun;
import sanbing.jcpp.app.dal.mapper.GunMapper;
import sanbing.jcpp.app.dal.repository.GunRepository;
@@ -30,10 +31,10 @@ import static sanbing.jcpp.infrastructure.util.validation.Validator.validateStri
*/
@Repository
@Slf4j
@RequiredArgsConstructor
public class GunRepositoryImpl extends CachedVersionedEntityRepository<GunCacheKey, Gun, GunCacheEvictEvent> implements GunRepository {
@Resource
GunMapper gunMapper;
private final GunMapper gunMapper;
@TransactionalEventListener(classes = GunCacheEvictEvent.class)
@Override
@@ -45,22 +46,36 @@ public class GunRepositoryImpl extends CachedVersionedEntityRepository<GunCacheK
if (event.getGunId() != null) {
toEvict.add(new GunCacheKey(event.getGunId()));
}
// 基于pileCode+gunCode的缓存key
if (event.getPileCode() != null && event.getGunCode() != null) {
toEvict.add(new GunCacheKey(event.getPileCode(), event.getGunCode()));
// 基于pileCode+gunNo的缓存key
if (event.getPileCode() != null && event.getGunNo() != null) {
toEvict.add(new GunCacheKey(event.getPileCode(), event.getGunNo()));
}
// 基于gunCode的缓存key
if (event.getGunCode() != null) {
toEvict.add(new GunCacheKey(event.getGunCode()));
}
cache.evict(toEvict);
}
@Override
public Gun findByPileCodeAndGunCode(String pileCode, String gunCode) {
public Gun findByPileCodeAndGunNo(String pileCode, String gunNo) {
validateString(pileCode, code -> "无效的桩编号: " + pileCode);
validateString(gunNo, no -> "无效的枪编号: " + gunNo);
return cache.get(new GunCacheKey(pileCode, gunNo),
() -> gunMapper.selectByPileCodeAndGunNo(pileCode, gunNo));
}
@Override
public Gun findByGunCode(String gunCode) {
validateString(gunCode, code -> "无效的枪编号: " + gunCode);
return cache.get(new GunCacheKey(pileCode, gunCode),
() -> gunMapper.selectByPileCodeAndGunCode(pileCode, gunCode));
return cache.get(new GunCacheKey(gunCode),
() -> gunMapper.selectByGunCode(gunCode));
}
@Override
@@ -70,4 +85,12 @@ public class GunRepositoryImpl extends CachedVersionedEntityRepository<GunCacheK
return cache.get(new GunCacheKey(gunId),
() -> gunMapper.selectById(gunId));
}
@Override
public GunWithStatusResponse findGunWithStatusByCode(String gunCode) {
validateString(gunCode, code -> "无效的枪编号: " + gunCode);
// 这个方法不使用缓存,因为它包含状态信息,需要实时查询
return gunMapper.selectGunWithStatusByCode(gunCode);
}
}

View File

@@ -6,7 +6,7 @@
*/
package sanbing.jcpp.app.dal.repository.impl;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.event.TransactionalEventListener;
@@ -26,10 +26,10 @@ import static sanbing.jcpp.infrastructure.util.validation.Validator.validateStri
*/
@Repository
@Slf4j
@RequiredArgsConstructor
public class PileRepositoryImpl extends CachedVersionedEntityRepository<PileCacheKey, Pile, PileCacheEvictEvent> implements PileRepository {
@Resource
PileMapper pileMapper;
private final PileMapper pileMapper;
@TransactionalEventListener(classes = PileCacheEvictEvent.class)
@Override

View File

@@ -42,11 +42,20 @@ public interface GunService {
* 分页查询充电枪及状态信息
*/
PageResponse<GunWithStatusResponse> queryGunsWithStatus(GunQueryRequest request);
/**
* 根据充电桩编码和充电枪编查询充电枪
* 根据充电桩编码和充电枪编查询充电枪
* 充电桩上报的是 pile_code + gun_no 的组合,这个组合是唯一的
*/
Gun findByPileCodeAndGunCode(String pileCode, String gunCode);
Gun findByPileCodeAndGunNo(String pileCode, String gunNo);
/**
* 根据枪编号查询充电枪
*/
Gun findByGunCode(String gunCode);
GunWithStatusResponse findGunWithStatusByCode(String gunCode);
/**
* 查询充电枪状态
@@ -69,11 +78,11 @@ public interface GunService {
* 处理充电枪状态上报
*
* @param pileCode 充电桩编码
* @param gunCode 充电枪编
* @param gunNo 充电枪编号 (如: "01", "02")
* @param protoStatus Proto状态
* @param ts 时间戳
* @return 是否需要更新充电桩状态
*/
boolean handleGunRunStatus(String pileCode, String gunCode, GunRunStatus protoStatus, long ts);
boolean handleGunRunStatus(String pileCode, String gunNo, GunRunStatus protoStatus, long ts);
}

View File

@@ -6,17 +6,14 @@
*/
package sanbing.jcpp.app.service;
import sanbing.jcpp.app.adapter.dto.*;
import sanbing.jcpp.infrastructure.queue.Callback;
import sanbing.jcpp.proto.gen.DownlinkProto;
import sanbing.jcpp.proto.gen.DownlinkProto.OfflineCardBalanceUpdateRequest;
import sanbing.jcpp.proto.gen.DownlinkProto.OfflineCardSyncRequest;
import sanbing.jcpp.proto.gen.DownlinkProto.OtaRequest;
import sanbing.jcpp.proto.gen.DownlinkProto.SetPricingRequest;
import sanbing.jcpp.proto.gen.UplinkProto.UplinkQueueMessage;
import sanbing.jcpp.proto.gen.DownlinkProto.SetQrcodeRequest;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import sanbing.jcpp.proto.gen.UplinkProto.UplinkQueueMessage;
/**
* @author 九筒
@@ -87,23 +84,22 @@ public interface PileProtocolService {
* 启动充电(支持卡号和并充序号)
* 当 parallelNo 不为空时,自动使用并充启机命令
*/
void startCharge(String pileCode, String gunCode, BigDecimal limitYuan, String orderNo,
String logicalCardNo, String physicalCardNo, String parallelNo);
void startCharge(StartChargeDTO startChargeDto);
/**
* 停止充电
*/
void stopCharge(String pileCode, String gunCode);
void stopCharge(StopChargeDTO stopChargeDto);
/**
* 重启充电
* 重启充电
*/
void restartPile(String pileCode, Integer type);
void restartPile(RestartPileDTO restartPileDto);
/**
* 下发计费
* 下发计费策略
*/
void setPricing(String pileCode, SetPricingRequest setPricingRequest);
void setPricing(SetPricingDTO setPricingDto);
/**
* 充电桩与 BMS 充电错误上报
@@ -179,7 +175,7 @@ public interface PileProtocolService {
/**
* 实时同步桩时间
*/
void timeSync(String pileCode, LocalDateTime time);
void timeSync(TimeSyncDTO timeSyncDto);
/**
* 实时同步桩时间应答

View File

@@ -32,6 +32,11 @@ public interface PileService {
* 根据ID查询充电桩
*/
Pile findById(UUID id);
/**
* 根据桩编号查询充电桩
*/
Pile findByPileCode(String pileCode);
/**
* 更新充电桩

View File

@@ -15,5 +15,6 @@ public class GunCacheEvictEvent {
private UUID gunId;
private String pileCode;
private String gunNo;
private String gunCode;
}

View File

@@ -13,27 +13,33 @@ import java.io.Serial;
import java.util.UUID;
@Builder
public record GunCacheKey(UUID gunId, String pileCode, String gunCode) implements VersionedCacheKey {
public record GunCacheKey(UUID gunId, String pileCode, String gunNo, String gunCode) implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 1L;
public GunCacheKey(UUID gunId) {
this(gunId, null, null);
this(gunId, null, null, null);
}
public GunCacheKey(String pileCode, String gunCode) {
this(null, pileCode, gunCode);
public GunCacheKey(String pileCode, String gunNo) {
this(null, pileCode, gunNo, null);
}
public GunCacheKey(String gunCode) {
this(null, null, null, gunCode);
}
@Override
public String toString() {
if (gunId != null) {
return gunId.toString();
} else if (pileCode != null && gunCode != null) {
return pileCode + ":" + gunCode;
} else if (pileCode != null && gunNo != null) {
return pileCode + ":" + gunNo;
} else if (gunCode != null) {
return "gunCode:" + gunCode;
} else {
throw new IllegalStateException("GunCacheKey 必须包含有效的 gunId 或者 pileCode+gunCode 组合");
throw new IllegalStateException("GunCacheKey 必须包含有效的 gunIdpileCode+gunNo 组合或者 gunCode");
}
}

View File

@@ -126,9 +126,20 @@ public class DefaultGunService implements GunService {
.build();
}
@Override
public Gun findByPileCodeAndGunCode(String pileCode, String gunCode) {
return gunRepository.findByPileCodeAndGunCode(pileCode, gunCode);
public Gun findByPileCodeAndGunNo(String pileCode, String gunNo) {
return gunRepository.findByPileCodeAndGunNo(pileCode, gunNo);
}
@Override
public Gun findByGunCode(String gunCode) {
return gunRepository.findByGunCode(gunCode);
}
@Override
public GunWithStatusResponse findGunWithStatusByCode(String gunCode) {
return gunRepository.findGunWithStatusByCode(gunCode);
}
@Override
@@ -167,33 +178,33 @@ public class DefaultGunService implements GunService {
}
@Override
public boolean handleGunRunStatus(String pileCode, String gunCode, GunRunStatus protoStatus, long ts) {
log.info("处理充电枪状态上报: 桩编码={}, 枪编={}, 状态={}", pileCode, gunCode, protoStatus);
public boolean handleGunRunStatus(String pileCode, String gunNo, GunRunStatus protoStatus, long ts) {
log.info("处理充电枪状态上报: 桩编码={}, 枪编={}, 状态={}", pileCode, gunNo, protoStatus);
// 将Proto状态转换为数据库枚举
GunRunStatusEnum dbStatus = convertProtoStatusToDbStatus(protoStatus);
if (dbStatus != null) {
// 获取充电枪信息(使用缓存)
Gun gun = findByPileCodeAndGunCode(pileCode, gunCode);
Gun gun = findByPileCodeAndGunNo(pileCode, gunNo);
if (gun != null) {
// 检查状态是否真的发生了变化,避免重复保存
String currentStatus = findGunStatus(gun.getId());
if (dbStatus.name().equals(currentStatus)) {
log.debug("充电枪状态未发生变化,跳过更新: 桩编码={}, 枪编={}, 状态={}", pileCode, gunCode, dbStatus);
log.debug("充电枪状态未发生变化,跳过更新: 桩编码={}, 枪编={}, 状态={}", pileCode, gunNo, dbStatus);
return false;
}
// 保存充电枪状态到属性表
saveGunStatusChange(gun.getId(), dbStatus.name(), ts);
log.info("充电枪状态更新成功: 桩编码={}, 枪编={}, 原状态={}, 新状态={}",
pileCode, gunCode, currentStatus, dbStatus);
log.info("充电枪状态更新成功: 桩编码={}, 枪编={}, 原状态={}, 新状态={}",
pileCode, gunNo, currentStatus, dbStatus);
// 根据充电枪状态判断是否需要更新充电桩状态
return shouldUpdatePileStatus(dbStatus);
} else {
log.warn("未找到充电枪: 桩编码={}, 枪编={}", pileCode, gunCode);
log.warn("未找到充电枪: 桩编码={}, 枪编={}", pileCode, gunNo);
}
} else {
log.warn("未知的充电枪状态: {}, 跳过更新", protoStatus);

View File

@@ -13,6 +13,7 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.adapter.dto.*;
import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum;
import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum;
import sanbing.jcpp.app.dal.entity.Gun;
@@ -309,12 +310,12 @@ public class DefaultPileProtocolService implements PileProtocolService {
try {
GunRunStatusProto gunRunStatusProto = uplinkQueueMessage.getGunRunStatusProto();
String pileCode = gunRunStatusProto.getPileCode();
String gunCode = gunRunStatusProto.getGunCode();
String gunNo = gunRunStatusProto.getGunNo();
long ts = uplinkQueueMessage.getTs();
GunRunStatus protoStatus = gunRunStatusProto.getGunRunStatus();
// 委托给 GunService 处理充电枪状态逻辑
boolean needUpdatePileStatus = gunService.handleGunRunStatus(pileCode, gunCode, protoStatus, ts);
boolean needUpdatePileStatus = gunService.handleGunRunStatus(pileCode, gunNo, protoStatus, ts);
// 如果需要,根据充电枪状态更新充电桩状态
if (needUpdatePileStatus) {
@@ -401,7 +402,7 @@ public class DefaultPileProtocolService implements PileProtocolService {
StartChargeRequest startChargeRequest = uplinkQueueMessage.getStartChargeRequest();
String pileCode = startChargeRequest.getPileCode();
String gunCode = startChargeRequest.getGunCode();
String gunNo = startChargeRequest.getGunNo();
// TODO 处理相关业务逻辑
String orderNo = "ORD" + RandomStringUtils.secure().nextNumeric(20);
String logicalCardNo = RandomStringUtils.secure().nextNumeric(12);
@@ -413,29 +414,35 @@ public class DefaultPileProtocolService implements PileProtocolService {
downlinkMessageBuilder.setStartChargeResponse(StartChargeResponse.newBuilder()
.setTradeNo(orderNo)
.setPileCode(startChargeRequest.getPileCode())
.setGunCode(startChargeRequest.getGunCode())
.setGunNo(startChargeRequest.getGunNo())
.setLogicalCardNo(logicalCardNo)
.setLimitYuan("50")
.setAuthSuccess(true)
.setFailReason(FailReason.ACCOUNT_NOT_ALLOWED_ON_PILE.name())
);
log.info("业务[充电桩启动充电请求应答] 发送下行消息到充电桩: {}, 充电枪: {}", pileCode, gunCode);
log.info("业务[充电桩启动充电请求应答] 发送下行消息到充电桩: {}, 充电枪: {}", pileCode, gunNo);
downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder,pileCode);
callback.onSuccess();
}
@Override
public void startCharge(String pileCode, String gunCode, BigDecimal limitYuan, String orderNo,
String logicalCardNo, String physicalCardNo, String parallelNo) {
public void startCharge(StartChargeDTO startChargeDto) {
String pileCode = startChargeDto.getPileCode();
String gunNo = startChargeDto.getGunNo();
BigDecimal limitYuan = startChargeDto.getLimitYuan();
String orderNo = startChargeDto.getOrderNo();
String logicalCardNo = startChargeDto.getLogicalCardNo();
String physicalCardNo = startChargeDto.getPhysicalCardNo();
String parallelNo = startChargeDto.getParallelNo();
UUID messageId = UUID.randomUUID();
UUID requestId = UUID.randomUUID();
RemoteStartChargingRequest.Builder requestBuilder = RemoteStartChargingRequest.newBuilder()
.setPileCode(pileCode)
.setGunCode(gunCode)
.setGunNo(gunNo)
.setLimitYuan(limitYuan.toPlainString())
.setTradeNo(orderNo);
@@ -464,12 +471,14 @@ public class DefaultPileProtocolService implements PileProtocolService {
.setDownlinkCmd(downlinkCmd.name())
.setRemoteStartChargingRequest(requestBuilder.build());
log.info("业务[远程启动充电] 发送下行消息到充电桩: {}, 充电枪: {}, 订单号: {}", pileCode, gunCode, orderNo);
log.info("业务[远程启动充电] 发送下行消息到充电桩: {}, 充电枪: {}, 订单号: {}", pileCode, gunNo, orderNo);
downlinkCallService.sendDownlinkMessage(downlinkRequestMessageBuilder, pileCode);
}
@Override
public void stopCharge(String pileCode, String gunCode) {
public void stopCharge(StopChargeDTO stopChargeDto) {
String pileCode = stopChargeDto.getPileCode();
String gunNo = stopChargeDto.getGunNo();
UUID messageId = UUID.randomUUID();
UUID requestId = UUID.randomUUID();
@@ -483,20 +492,21 @@ public class DefaultPileProtocolService implements PileProtocolService {
.setDownlinkCmd(DownlinkCmdEnum.REMOTE_STOP_CHARGING.name())
.setRemoteStopChargingRequest(RemoteStopChargingRequest.newBuilder()
.setPileCode(pileCode)
.setGunCode(gunCode)
.setGunNo(gunNo)
.build());
log.info("业务[远程停止充电] 发送下行消息到充电桩: {}, 充电枪: {}", pileCode, gunCode);
log.info("业务[远程停止充电] 发送下行消息到充电桩: {}, 充电枪: {}", pileCode, gunNo);
downlinkCallService.sendDownlinkMessage(downlinkRequestMessageBuilder, pileCode);
}
@Override
public void restartPile(String pileCode, Integer type) {
public void restartPile(RestartPileDTO restartPileDto) {
String pileCode = restartPileDto.getPileCode();
Integer type = restartPileDto.getType();
UUID messageId = UUID.randomUUID();
UUID requestId = UUID.randomUUID();
DownlinkRequestMessage.Builder downlinkRequestMessageBuilder = DownlinkRequestMessage.newBuilder()
.setMessageIdMSB(messageId.getMostSignificantBits())
.setMessageIdLSB(messageId.getLeastSignificantBits())
@@ -514,7 +524,10 @@ public class DefaultPileProtocolService implements PileProtocolService {
}
@Override
public void setPricing(String pileCode, SetPricingRequest setPricingRequest) {
public void setPricing(SetPricingDTO setPricingDto) {
String pileCode = setPricingDto.getPileCode();
SetPricingRequest setPricingRequest = setPricingDto.getSetPricingRequest();
UUID messageId = UUID.randomUUID();
UUID requestId = UUID.randomUUID();
@@ -557,9 +570,9 @@ public class DefaultPileProtocolService implements PileProtocolService {
BmsChargingInfoProto bmsCharingInfoProto = uplinkQueueMessage.getBmsChargingInfoProto();
String tradeNo = bmsCharingInfoProto.getTradeNo();
String pileCode = bmsCharingInfoProto.getPileCode();
String gunCode = bmsCharingInfoProto.getGunCode();
String gunNo = bmsCharingInfoProto.getGunNo();
String additionalInfo = bmsCharingInfoProto.getAdditionalInfo();
log.info("BMS充电信息: 交易流水号: {}, 桩编码: {}, 枪号: {}, 附加信息: {}", tradeNo, pileCode, gunCode, additionalInfo);
log.info("BMS充电信息: 交易流水号: {}, 桩编码: {}, 枪号: {}, 附加信息: {}", tradeNo, pileCode, gunNo, additionalInfo);
// TODO 处理相关业务逻辑
callback.onSuccess();
}
@@ -609,7 +622,7 @@ public class DefaultPileProtocolService implements PileProtocolService {
BmsHandshakeProto bmsHandshakeProto = uplinkQueueMessage.getBmsHandshakeProto();
String tradeNo = bmsHandshakeProto.getTradeNo();
String pileCode = bmsHandshakeProto.getPileCode();
String gunCode = bmsHandshakeProto.getGunCode();
String gunNo = bmsHandshakeProto.getGunNo();
String carVinCode = bmsHandshakeProto.getCarVinCode();
String bmsProtocolVersion = bmsHandshakeProto.getBmsProtocolVersion();
int bmsBatteryType = bmsHandshakeProto.getBmsBatteryType();
@@ -617,8 +630,8 @@ public class DefaultPileProtocolService implements PileProtocolService {
String additionalInfo = bmsHandshakeProto.getAdditionalInfo();
log.info("BMS充电握手信息: 交易流水号: {}, 桩编码: {}, 枪号: {}, 车辆VIN: {}, BMS协议版本: {}, " +
"电池类型: {}, 电池容量: {}Ah, 附加信息: {}",
tradeNo, pileCode, gunCode, carVinCode, bmsProtocolVersion,
"电池类型: {}, 电池容量: {}Ah, 附加信息: {}",
tradeNo, pileCode, gunNo, carVinCode, bmsProtocolVersion,
bmsBatteryType, bmsPowerCapacity, additionalInfo);
// TODO 处理相关业务逻辑,比如保存握手信息到数据库
@@ -627,7 +640,10 @@ public class DefaultPileProtocolService implements PileProtocolService {
}
@Override
public void timeSync(String pileCode, LocalDateTime time) {
public void timeSync(TimeSyncDTO timeSyncDto) {
String pileCode = timeSyncDto.getPileCode();
LocalDateTime time = timeSyncDto.getTime();
UUID messageId = UUID.randomUUID();
UUID requestId = UUID.randomUUID();
DownlinkRequestMessage.Builder downlinkRequestMessageBuilder = DownlinkRequestMessage.newBuilder()
@@ -689,32 +705,33 @@ public class DefaultPileProtocolService implements PileProtocolService {
log.info("接收到地锁状态信息 {}", uplinkQueueMessage);
GroundLockStatusProto groundLockStatusProto = uplinkQueueMessage.getGroundLockStatusProto();
String pileCode = groundLockStatusProto.getPileCode();
String gunCode = groundLockStatusProto.getGunCode();
String gunNo = groundLockStatusProto.getGunNo();
int lockStatus = groundLockStatusProto.getLockStatus();
int parkStatus = groundLockStatusProto.getParkStatus();
int lockBattery = groundLockStatusProto.getLockBattery();
int alarmStatus = groundLockStatusProto.getAlarmStatus();
log.info("地锁状态信息: 桩编码: {}, 枪号: {}, 车位锁状态: {}, 车位状态: {}, 地锁电量: {}%, 报警状态: {}",
pileCode, gunCode, lockStatus, parkStatus, lockBattery, alarmStatus);
pileCode, gunNo, lockStatus, parkStatus, lockBattery, alarmStatus);
try {
// 获取时间戳
long ts = uplinkQueueMessage.getTs();
// 获取充电枪信息
Gun gun = gunService.findByPileCodeAndGunCode(pileCode, gunCode);
// 注意充电桩上报的gunNo实际上是枪编号(gun_no),不是完整的枪编码(gun_code)
Gun gun = gunService.findByPileCodeAndGunNo(pileCode, gunNo);
if (gun != null) {
// 保存地锁状态到属性表
saveLockStatusToAttributes(gun.getId(), lockStatus, parkStatus, lockBattery, alarmStatus, ts);
log.info("地锁和车位状态已保存: 桩编码={}, 枪编码={}, 地锁状态={}, 车位状态={}",
pileCode, gunCode, lockStatus, parkStatus);
pileCode, gunNo, lockStatus, parkStatus);
} else {
log.warn("未找到充电枪,无法保存地锁状态: 桩编码={}, 枪编码={}", pileCode, gunCode);
log.warn("未找到充电枪,无法保存地锁状态: 桩编码={}, 枪编码={}", pileCode, gunNo);
}
} catch (Exception e) {
log.error("保存地锁状态失败: 桩编码={}, 枪编码={}", pileCode, gunCode, e);
log.error("保存地锁状态失败: 桩编码={}, 枪编码={}", pileCode, gunNo, e);
callback.onFailure(e);
return;
}
@@ -855,11 +872,11 @@ public class DefaultPileProtocolService implements PileProtocolService {
log.info("接收到充电过程BMS需求与充电机输出信息:{}", uplinkQueueMessage);
BmsDemandChargerOutputProto bmsDemandChargerOutputProto = uplinkQueueMessage.getBmsDemandChargerOutputProto();
String pileCode = bmsDemandChargerOutputProto.getPileCode();
String gunCode = bmsDemandChargerOutputProto.getGunCode();
String gunNo = bmsDemandChargerOutputProto.getGunNo();
String tradeNo = bmsDemandChargerOutputProto.getTradeNo();
String additionalInfo = bmsDemandChargerOutputProto.getAdditionalInfo();
log.info("充电过程BMS需求与充电机输出信息: 桩编码: {}, 枪号: {}, 交易流水号: {}, 附加信息: {}",
pileCode, gunCode, tradeNo, additionalInfo);
pileCode, gunNo, tradeNo, additionalInfo);
// TODO 处理相关业务逻辑
callback.onSuccess();
}

View File

@@ -82,6 +82,11 @@ public class DefaultPileService implements PileService {
return pileMapper.selectById(id);
}
@Override
public Pile findByPileCode(String pileCode) {
return pileRepository.findPileByCode(pileCode);
}
@Override
public Pile updatePile(UUID id, PileUpdateRequest request) throws JCPPException {
Pile existingPile = findById(id);

View File

@@ -98,11 +98,62 @@
</choose>
</select>
<!-- 根据充电桩编码和充电枪编码查询充电枪 -->
<select id="selectByPileCodeAndGunCode" resultType="sanbing.jcpp.app.dal.entity.Gun">
<!-- 根据充电桩编码和充电枪编号查询充电枪 -->
<select id="selectByPileCodeAndGunNo" resultType="sanbing.jcpp.app.dal.entity.Gun">
SELECT g.* FROM t_gun g
INNER JOIN t_pile p ON g.pile_id = p.id
WHERE p.pile_code = #{pileCode} AND g.gun_code = #{gunCode}
INNER JOIN t_pile p ON g.pile_id = p.id
WHERE p.pile_code = #{pileCode}
AND g.gun_no = #{gunNo}
</select>
<!-- 根据枪编号查询充电枪 -->
<select id="selectByGunCode" resultType="sanbing.jcpp.app.dal.entity.Gun">
SELECT *
FROM t_gun
WHERE gun_code = #{gunCode}
</select>
<!-- 根据枪编号查询充电枪详细信息(包含关联信息) -->
<select id="selectGunWithStatusByCode" resultType="sanbing.jcpp.app.adapter.response.GunWithStatusResponse">
SELECT
<!-- 充电枪基本信息 -->
g.id,
g.created_time,
g.updated_time,
g.gun_name,
g.gun_no,
g.gun_code,
g.station_id,
g.pile_id,
g.additional_info,
g.version,
<!-- 充电站信息 -->
s.station_name,
<!-- 充电桩信息 -->
p.pile_name,
p.pile_code,
<!-- 状态相关属性 -->
a_run_status.str_v as run_status
FROM t_gun g
<!-- LEFT JOIN 获取充电站信息 -->
LEFT JOIN t_station s ON g.station_id = s.id
<!-- LEFT JOIN 获取充电桩信息 -->
LEFT JOIN t_pile p ON g.pile_id = p.id
<!-- LEFT JOIN 获取充电枪运行状态属性 -->
LEFT JOIN t_attr a_run_status ON (
a_run_status.entity_id = g.id AND
a_run_status.attr_key = 'gunRunStatus'
)
WHERE g.gun_code = #{gunCode}
</select>
<!-- 统计充电桩下的充电枪数量 -->

View File

@@ -10,7 +10,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sanbing.jcpp.app.dal.mapper.PileMapper">
<!-- 充电桩带状态信息的分页查询 -->
<!-- 充电桩带状态信息的分页查询 - 性能优化版本 -->
<select id="selectPileWithStatusPage" resultType="sanbing.jcpp.app.adapter.response.PileWithStatusResponse">
SELECT
<!-- 充电桩基本信息 -->
@@ -27,38 +27,30 @@
p.type,
p.additional_info,
p.version,
<!-- 状态相关属性 - 使用CASE WHEN优化多次JOIN -->
MAX(CASE WHEN a.attr_key = #{statusKey.code} THEN a.str_v END) as status,
MAX(CASE WHEN a.attr_key = #{connectedAtKey.code} THEN a.last_update_ts END) as connected_at,
MAX(CASE WHEN a.attr_key = #{disconnectedAtKey.code} THEN a.last_update_ts END) as disconnected_at,
MAX(CASE WHEN a.attr_key = #{lastActiveTimeKey.code} THEN a.last_update_ts END) as last_active_time,
<!-- 充电枪数量 - 使用LEFT JOIN优化子查询 -->
COALESCE(g.gun_count, 0) as gun_count
<!-- 状态相关属性 -->
a_status.str_v as status,
a_connected.last_update_ts as connected_at,
a_disconnected.last_update_ts as disconnected_at,
a_last_active.last_update_ts as last_active_time
FROM t_pile p
<!-- LEFT JOIN 获取充电桩状态属性 -->
LEFT JOIN t_attr a_status ON (
a_status.entity_id = p.id AND
a_status.attr_key = #{statusKey.code}
)
<!-- LEFT JOIN 获取最后连接时间属性 -->
LEFT JOIN t_attr a_connected ON (
a_connected.entity_id = p.id AND
a_connected.attr_key = #{connectedAtKey.code}
)
<!-- LEFT JOIN 获取最后断开时间属性 -->
LEFT JOIN t_attr a_disconnected ON (
a_disconnected.entity_id = p.id AND
a_disconnected.attr_key = #{disconnectedAtKey.code}
)
<!-- LEFT JOIN 获取最后活跃时间属性 -->
LEFT JOIN t_attr a_last_active ON (
a_last_active.entity_id = p.id AND
a_last_active.attr_key = #{lastActiveTimeKey.code}
FROM t_pile p
<!-- 单次JOIN获取所有属性避免多次JOIN -->
LEFT JOIN t_attr a ON (
a.entity_id = p.id AND
a.attr_key IN (#{statusKey.code}, #{connectedAtKey.code}, #{disconnectedAtKey.code}, #{lastActiveTimeKey.code})
)
<!-- LEFT JOIN 获取充电枪数量统计 -->
LEFT JOIN (
SELECT pile_id, COUNT(*) as gun_count
FROM t_gun
GROUP BY pile_id
) g ON g.pile_id = p.id
<!-- 动态WHERE条件 -->
<where>
@@ -87,9 +79,18 @@
AND p.type = #{request.type}
</if>
<if test="request.status != null">
AND EXISTS (
SELECT 1 FROM t_attr a_status
WHERE a_status.entity_id = p.id
AND a_status.attr_key = #{statusKey.code}
AND a_status.str_v = #{request.status.code}
)
</if>
</where>
<!-- GROUP BY 子句 - 因为使用了聚合函数 -->
GROUP BY p.id, p.created_time, p.updated_time, p.pile_name, p.pile_code, p.protocol,
p.station_id, p.brand, p.model, p.manufacturer, p.type, p.additional_info, p.version, g.gun_count
<!-- 动态ORDER BY -->
ORDER BY
@@ -116,16 +117,19 @@
p.type ${request.sortOrder}
</when>
<when test="request.sortField == 'status'">
a_status.str_v ${request.sortOrder}
MAX(CASE WHEN a.attr_key = #{statusKey.code} THEN a.str_v END) ${request.sortOrder}
</when>
<when test="request.sortField == 'connectedAt'">
a_connected.last_update_ts ${request.sortOrder}
MAX(CASE WHEN a.attr_key = #{connectedAtKey.code} THEN a.last_update_ts END) ${request.sortOrder}
</when>
<when test="request.sortField == 'disconnectedAt'">
a_disconnected.last_update_ts ${request.sortOrder}
MAX(CASE WHEN a.attr_key = #{disconnectedAtKey.code} THEN a.last_update_ts END) ${request.sortOrder}
</when>
<when test="request.sortField == 'lastActiveTime'">
a_last_active.last_update_ts ${request.sortOrder}
MAX(CASE WHEN a.attr_key = #{lastActiveTimeKey.code} THEN a.last_update_ts END) ${request.sortOrder}
</when>
<when test="request.sortField == 'gunCount'">
COALESCE(g.gun_count, 0) ${request.sortOrder}
</when>
<when test="request.sortField == 'createdTime'">
p.created_time ${request.sortOrder}

View File

@@ -96,11 +96,13 @@ CREATE TABLE IF NOT EXISTS t_gun
version int default 1
);
CREATE INDEX IF NOT EXISTS idx_gun_pile_id
on t_gun (pile_id);
-- pile_id + gun_no 唯一索引,确保同一充电桩下枪号唯一
CREATE UNIQUE INDEX IF NOT EXISTS uni_gun_pile_gun_no
on t_gun (pile_id, gun_no);
CREATE INDEX IF NOT EXISTS idx_gun_pile_gun_code
on t_gun (pile_id, gun_code);
-- gun_code 唯一索引,确保充电枪编码全局唯一
CREATE UNIQUE INDEX IF NOT EXISTS uni_gun_code
on t_gun (gun_code);