mirror of
https://gitee.com/san-bing/JChargePointProtocol
synced 2026-05-05 18:39:56 +08:00
* !44 comment * !39 添加下行日志打印 * !36 扩展计价领域模型 * !35 webui 初步成型 * !34 webui 初步成型
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "spring.mvc.cors")
|
||||
public class MvcCorsProperties {
|
||||
|
||||
private Map<String, CorsConfiguration> mappings = new HashMap<>();
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import sanbing.jcpp.app.exception.JCPPErrorCode;
|
||||
import sanbing.jcpp.app.exception.JCPPErrorResponseHandler;
|
||||
import sanbing.jcpp.app.exception.JCPPException;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 基础控制器
|
||||
* 提供统一的异常处理机制,所有Controller都应该继承此类
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class BaseController {
|
||||
|
||||
@Autowired
|
||||
private JCPPErrorResponseHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* 处理所有通用异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public void handleControllerException(Exception e, HttpServletResponse response) {
|
||||
log.debug("Processing controller exception: {}", e.getMessage(), e);
|
||||
|
||||
JCPPException jcppException = handleException(e);
|
||||
|
||||
// 如果是通用错误且有具体的原因异常,则使用原始异常
|
||||
if (jcppException.getErrorCode() == JCPPErrorCode.GENERAL &&
|
||||
jcppException.getCause() instanceof Exception &&
|
||||
Objects.equals(jcppException.getCause().getMessage(), jcppException.getMessage())) {
|
||||
e = (Exception) jcppException.getCause();
|
||||
} else {
|
||||
e = jcppException;
|
||||
}
|
||||
|
||||
errorResponseHandler.handle(e, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理JCPPException异常
|
||||
* 直接委托给统一的错误处理器
|
||||
*/
|
||||
@ExceptionHandler(JCPPException.class)
|
||||
public void handleJCPPException(JCPPException ex, HttpServletResponse response) {
|
||||
log.debug("Processing JCPP exception: {}", ex.getMessage(), ex);
|
||||
errorResponseHandler.handle(ex, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
* 将Spring的验证异常转换为JCPPException
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) {
|
||||
log.warn("Validation error occurred: {}", validationError.getMessage());
|
||||
|
||||
// 提取字段错误信息
|
||||
String errorMessage = validationError.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
if (errorMessage.isEmpty()) {
|
||||
errorMessage = "Validation failed";
|
||||
} else {
|
||||
errorMessage = "Validation error: " + errorMessage;
|
||||
}
|
||||
|
||||
JCPPException jcppException = new JCPPException(errorMessage, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
handleControllerException(jcppException, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常转换处理方法
|
||||
* 将各种异常转换为JCPPException,统一异常处理流程
|
||||
*/
|
||||
private JCPPException handleException(Exception e) {
|
||||
if (e instanceof JCPPException jcppException) {
|
||||
return jcppException;
|
||||
}
|
||||
|
||||
// 处理运行时异常
|
||||
if (e instanceof RuntimeException) {
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
return new JCPPException("Invalid argument: " + e.getMessage(), e, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
} else if (e instanceof IllegalStateException) {
|
||||
return new JCPPException("Invalid state: " + e.getMessage(), e, JCPPErrorCode.VERSION_CONFLICT);
|
||||
} else {
|
||||
return new JCPPException("Runtime error: " + e.getMessage(), e, JCPPErrorCode.GENERAL);
|
||||
}
|
||||
}
|
||||
|
||||
// 其他异常统一处理为通用错误
|
||||
return new JCPPException("Unexpected error: " + e.getMessage(), e, JCPPErrorCode.GENERAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.response.ApiResponse;
|
||||
import sanbing.jcpp.app.adapter.response.DashboardStats;
|
||||
import sanbing.jcpp.app.service.DashboardService;
|
||||
|
||||
/**
|
||||
* 仪表盘控制器
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController extends BaseController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<ApiResponse<DashboardStats>> getStats() {
|
||||
DashboardStats stats = dashboardService.getDashboardStats();
|
||||
return ResponseEntity.ok(ApiResponse.success("获取仪表盘数据成功", stats));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import sanbing.jcpp.app.adapter.request.GunCreateRequest;
|
||||
import sanbing.jcpp.app.adapter.request.GunQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.request.GunUpdateRequest;
|
||||
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.service.GunService;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/guns")
|
||||
@RequiredArgsConstructor
|
||||
public class GunController extends BaseController {
|
||||
|
||||
private final GunService gunService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Gun>> createGun(@Valid @RequestBody GunCreateRequest request) {
|
||||
Gun gun = gunService.createGun(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("创建成功", gun));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Gun>> getGun(@PathVariable UUID id) {
|
||||
Gun gun = gunService.findById(id);
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", gun));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Gun>> updateGun(@PathVariable UUID id,
|
||||
@Valid @RequestBody GunUpdateRequest request) {
|
||||
Gun gun = gunService.updateGun(id, request);
|
||||
return ResponseEntity.ok(ApiResponse.success("更新成功", gun));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteGun(@PathVariable UUID id) {
|
||||
gunService.deleteGun(id);
|
||||
return ResponseEntity.ok(ApiResponse.success("删除成功", null));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<PageResponse<GunWithStatusResponse>>> queryGunsWithStatus(GunQueryRequest request) {
|
||||
PageResponse<GunWithStatusResponse> guns = gunService.queryGunsWithStatus(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", guns));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import sanbing.jcpp.app.adapter.request.PileCreateRequest;
|
||||
import sanbing.jcpp.app.adapter.request.PileQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.request.PileUpdateRequest;
|
||||
import sanbing.jcpp.app.adapter.response.ApiResponse;
|
||||
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.exception.JCPPException;
|
||||
import sanbing.jcpp.app.service.PileService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/piles")
|
||||
@RequiredArgsConstructor
|
||||
public class PileController extends BaseController {
|
||||
|
||||
private final PileService pileService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Pile>> createPile(@Valid @RequestBody PileCreateRequest request) {
|
||||
Pile pile = pileService.createPile(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("创建成功", pile));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Pile>> getPile(@PathVariable UUID id) {
|
||||
Pile pile = pileService.findById(id);
|
||||
if (pile == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", pile));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Pile>> updatePile(@PathVariable UUID id,
|
||||
@Valid @RequestBody PileUpdateRequest request) throws JCPPException {
|
||||
Pile pile = pileService.updatePile(id, request);
|
||||
return ResponseEntity.ok(ApiResponse.success("更新成功", pile));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deletePile(@PathVariable UUID id) throws JCPPException {
|
||||
pileService.deletePile(id);
|
||||
return ResponseEntity.ok(ApiResponse.success("删除成功", null));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<PageResponse<PileWithStatusResponse>>> queryPilesWithStatus(PileQueryRequest request) {
|
||||
PageResponse<PileWithStatusResponse> piles = pileService.queryPilesWithStatus(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", piles));
|
||||
}
|
||||
|
||||
@GetMapping("/options")
|
||||
public ResponseEntity<ApiResponse<List<PileOptionResponse>>> getPileOptions() {
|
||||
List<PileOptionResponse> options = pileService.getPileOptions();
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.response.ApiResponse;
|
||||
import sanbing.jcpp.app.adapter.response.ProtocolOption;
|
||||
import sanbing.jcpp.app.service.ProtocolService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 协议管理控制器
|
||||
*
|
||||
* @author 九筒
|
||||
* @since 2024-12-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/protocols")
|
||||
@RequiredArgsConstructor
|
||||
public class ProtocolController extends BaseController {
|
||||
|
||||
private final ProtocolService protocolService;
|
||||
|
||||
/**
|
||||
* 获取所有支持的协议列表
|
||||
*/
|
||||
@GetMapping("/supported")
|
||||
public ResponseEntity<ApiResponse<List<ProtocolOption>>> getSupportedProtocols() {
|
||||
List<ProtocolOption> protocols = protocolService.getSupportedProtocols();
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", protocols));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import sanbing.jcpp.app.adapter.request.StationCreateRequest;
|
||||
import sanbing.jcpp.app.adapter.request.StationQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.request.StationUpdateRequest;
|
||||
import sanbing.jcpp.app.adapter.response.ApiResponse;
|
||||
import sanbing.jcpp.app.adapter.response.PageResponse;
|
||||
import sanbing.jcpp.app.adapter.response.StationOption;
|
||||
import sanbing.jcpp.app.dal.entity.Station;
|
||||
import sanbing.jcpp.app.exception.JCPPException;
|
||||
import sanbing.jcpp.app.service.StationService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 充电站管理控制器
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/stations")
|
||||
@RequiredArgsConstructor
|
||||
public class StationController extends BaseController {
|
||||
|
||||
private final StationService stationService;
|
||||
|
||||
/**
|
||||
* 分页查询充电站
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<PageResponse<Station>>> getStations(StationQueryRequest request) {
|
||||
PageResponse<Station> result = stationService.getStations(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取充电站详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Station>> getStation(@PathVariable UUID id) {
|
||||
Station station = stationService.getStationById(id);
|
||||
if (station == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", station));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建充电站
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Station>> createStation(@Valid @RequestBody StationCreateRequest request) {
|
||||
Station station = stationService.createStation(request);
|
||||
return ResponseEntity.ok(ApiResponse.success("创建成功", station));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新充电站
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Station>> updateStation(@PathVariable UUID id,
|
||||
@Valid @RequestBody StationUpdateRequest request) throws JCPPException {
|
||||
Station station = stationService.updateStation(id, request);
|
||||
return ResponseEntity.ok(ApiResponse.success("更新成功", station));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除充电站
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteStation(@PathVariable UUID id) throws JCPPException {
|
||||
stationService.deleteStation(id);
|
||||
return ResponseEntity.ok(ApiResponse.success("删除成功", null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充电站选项列表(用于下拉选择)
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
public ResponseEntity<ApiResponse<List<StationOption>>> getStationOptions() {
|
||||
List<StationOption> options = stationService.getStationOptions();
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", options));
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索充电站选项列表(支持关键字搜索和分页)
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<ApiResponse<List<StationOption>>> searchStationOptions(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
List<StationOption> options = stationService.searchStationOptions(keyword, page, size);
|
||||
return ResponseEntity.ok(ApiResponse.success("查询成功", options));
|
||||
}
|
||||
}
|
||||
@@ -4,46 +4,48 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter;
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
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.service.PileProtocolService;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
@RestController
|
||||
public class TestController {
|
||||
@RequestMapping("/test")
|
||||
public class TestController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private PileProtocolService pileProtocolService;
|
||||
|
||||
@GetMapping("/api/startCharge")
|
||||
@GetMapping("/startCharge")
|
||||
public ResponseEntity<String> startCharge() {
|
||||
|
||||
String orderNo = "ORD" + RandomStringUtils.secure().nextNumeric(20);
|
||||
String logicalCardNo = RandomStringUtils.secure().nextNumeric(12);
|
||||
String physicalCardNo = RandomStringUtils.secure().nextNumeric(12);
|
||||
|
||||
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("50"), orderNo,
|
||||
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("50"), orderNo,
|
||||
logicalCardNo, physicalCardNo, null);
|
||||
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/parallelStartCharge")
|
||||
@GetMapping("/parallelStartCharge")
|
||||
public ResponseEntity<String> parallelStartCharge() {
|
||||
|
||||
String orderNo = "PAR" + RandomStringUtils.secure().nextNumeric(20);
|
||||
@@ -51,13 +53,13 @@ public class TestController {
|
||||
String physicalCardNo = RandomStringUtils.secure().nextNumeric(12);
|
||||
String parallelNo = RandomStringUtils.secure().nextNumeric(6);
|
||||
|
||||
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("100"),
|
||||
pileProtocolService.startCharge("20231212000010", "01", new BigDecimal("100"),
|
||||
orderNo, logicalCardNo, physicalCardNo, parallelNo);
|
||||
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/stopCharge")
|
||||
@GetMapping("/stopCharge")
|
||||
public ResponseEntity<String> stopCharge() {
|
||||
|
||||
pileProtocolService.stopCharge("20231212000010", "01");
|
||||
@@ -65,7 +67,7 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/restartPile")
|
||||
@GetMapping("/restartPile")
|
||||
public ResponseEntity<String> restartPile() {
|
||||
|
||||
pileProtocolService.restartPile("20231212000010", 1);
|
||||
@@ -73,7 +75,7 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/setPricing")
|
||||
@GetMapping("/setPricing")
|
||||
public ResponseEntity<String> setPricing() {
|
||||
|
||||
String pileCode = "20231212000010";
|
||||
@@ -159,12 +161,8 @@ public class TestController {
|
||||
flagPriceMap.put(PricingModelFlag.FLAT_VALUE, flagPriceFlat);
|
||||
flagPriceMap.put(PricingModelFlag.VALLEY_VALUE, flagPriceValley);
|
||||
|
||||
// 构建 PricingModelProto 对象
|
||||
PricingModelProto pricingModel = PricingModelProto.newBuilder()
|
||||
.setType(PricingModelType.CHARGE) // 设置为充电计费模型
|
||||
.setRule(PricingModelRule.SPLIT_TIME) // 使用分时计费规则
|
||||
.setStandardElec("1.0") // 标准电费(默认值)
|
||||
.setStandardServ("0.3") // 标准服务费(默认值)
|
||||
// 构建峰谷计价配置
|
||||
PeakValleyPricingProto peakValleyPricing = PeakValleyPricingProto.newBuilder()
|
||||
.putAllFlagPrice(flagPriceMap) // 设置尖峰平谷对应的价格
|
||||
.addPeriod(topPeriod1) // 添加尖峰时段1
|
||||
.addPeriod(topPeriod2) // 添加尖峰时段2
|
||||
@@ -174,6 +172,13 @@ public class TestController {
|
||||
.addPeriod(flatPeriod2) // 添加平时段2
|
||||
.addPeriod(valleyPeriod) // 添加谷时段
|
||||
.build();
|
||||
|
||||
// 构建 PricingModelProto 对象
|
||||
PricingModelProto pricingModel = PricingModelProto.newBuilder()
|
||||
.setType(PricingModelType.CHARGE) // 设置为充电计费模型
|
||||
.setRule(PricingModelRule.PEAK_VALLEY_PRICING) // 使用峰谷计费规则
|
||||
.setPeakValleyPricing(peakValleyPricing) // 设置峰谷计价配置
|
||||
.build();
|
||||
|
||||
pileProtocolService.setPricing(pileCode,
|
||||
SetPricingRequest.newBuilder()
|
||||
@@ -185,11 +190,89 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/timePeriodPricing")
|
||||
public ResponseEntity<String> testTimePeriodPricing() {
|
||||
String pileCode = "TEST001";
|
||||
|
||||
@GetMapping("/api/otaRequest")
|
||||
// 创建时段计价列表
|
||||
List<TimePeriodItemProto> timePeriodItems = new ArrayList<>();
|
||||
|
||||
// 深夜时段 (00:00-06:00)
|
||||
timePeriodItems.add(TimePeriodItemProto.newBuilder()
|
||||
.setPeriodNo(1)
|
||||
.setStartTime("00:00:00")
|
||||
.setEndTime("06:00:00")
|
||||
.setElecPrice("0.40")
|
||||
.setServPrice("0.20")
|
||||
.setDescription("深夜时段")
|
||||
.build());
|
||||
|
||||
// 早高峰时段 (06:00-10:00)
|
||||
timePeriodItems.add(TimePeriodItemProto.newBuilder()
|
||||
.setPeriodNo(2)
|
||||
.setStartTime("06:00:00")
|
||||
.setEndTime("10:00:00")
|
||||
.setElecPrice("0.80")
|
||||
.setServPrice("0.50")
|
||||
.setDescription("早高峰时段")
|
||||
.build());
|
||||
|
||||
// 日间平时段 (10:00-18:00)
|
||||
timePeriodItems.add(TimePeriodItemProto.newBuilder()
|
||||
.setPeriodNo(3)
|
||||
.setStartTime("10:00:00")
|
||||
.setEndTime("18:00:00")
|
||||
.setElecPrice("0.65")
|
||||
.setServPrice("0.35")
|
||||
.setDescription("日间平时段")
|
||||
.build());
|
||||
|
||||
// 晚高峰时段 (18:00-22:00)
|
||||
timePeriodItems.add(TimePeriodItemProto.newBuilder()
|
||||
.setPeriodNo(4)
|
||||
.setStartTime("18:00:00")
|
||||
.setEndTime("22:00:00")
|
||||
.setElecPrice("0.90")
|
||||
.setServPrice("0.60")
|
||||
.setDescription("晚高峰时段")
|
||||
.build());
|
||||
|
||||
// 夜间时段 (22:00-24:00)
|
||||
timePeriodItems.add(TimePeriodItemProto.newBuilder()
|
||||
.setPeriodNo(5)
|
||||
.setStartTime("22:00:00")
|
||||
.setEndTime("23:59:59")
|
||||
.setElecPrice("0.50")
|
||||
.setServPrice("0.25")
|
||||
.setDescription("夜间时段")
|
||||
.build());
|
||||
|
||||
// 构建时段计价配置
|
||||
TimePeriodPricingProto timePeriodPricing = TimePeriodPricingProto.newBuilder()
|
||||
.addAllPeriods(timePeriodItems)
|
||||
.build();
|
||||
|
||||
// 构建 PricingModelProto 对象
|
||||
PricingModelProto pricingModel = PricingModelProto.newBuilder()
|
||||
.setType(PricingModelType.CHARGE) // 设置为充电计费模型
|
||||
.setRule(PricingModelRule.TIME_PERIOD_PRICING) // 使用时段计价规则
|
||||
.setTimePeriodPricing(timePeriodPricing) // 设置时段计价配置
|
||||
.build();
|
||||
|
||||
pileProtocolService.setPricing(pileCode,
|
||||
SetPricingRequest.newBuilder()
|
||||
.setPileCode(pileCode)
|
||||
.setPricingId(2000L)
|
||||
.setPricingModel(pricingModel)
|
||||
.build());
|
||||
|
||||
return ResponseEntity.ok("Time period pricing test success");
|
||||
}
|
||||
|
||||
@GetMapping("/otaRequest")
|
||||
public ResponseEntity<String> otaRequest() {
|
||||
|
||||
pileProtocolService.otaRequest(ProtocolProto.OtaRequest.newBuilder()
|
||||
pileProtocolService.otaRequest(OtaRequest.newBuilder()
|
||||
.setAddress("127.0.0.1")
|
||||
.setExecutionControl(1)
|
||||
.setDownloadTimeout(1)
|
||||
@@ -205,7 +288,7 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/offlineCardBalanceUpdateRequest")
|
||||
@GetMapping("/offlineCardBalanceUpdateRequest")
|
||||
public ResponseEntity<String> offlineCardBalanceUpdateRequest() {
|
||||
|
||||
pileProtocolService.offlineCardBalanceUpdateRequest(OfflineCardBalanceUpdateRequest.newBuilder()
|
||||
@@ -218,7 +301,7 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/offlineCardSyncRequest")
|
||||
@GetMapping("/offlineCardSyncRequest")
|
||||
public ResponseEntity<String> offlineCardSyncRequest() {
|
||||
|
||||
List<CardInfo> cardInfos = Lists.newArrayList(CardInfo.newBuilder().setCardNo("1000000000123456").setLogicCardNo("1000000000123456").build(),
|
||||
@@ -234,7 +317,7 @@ public class TestController {
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping("/api/timeSync")
|
||||
@GetMapping("/timeSync")
|
||||
public ResponseEntity<String> timeSync() {
|
||||
pileProtocolService.timeSync("20231212000010", LocalDateTime.now());
|
||||
return ResponseEntity.ok("success");
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.response.ApiResponse;
|
||||
import sanbing.jcpp.app.adapter.response.ErrorCode;
|
||||
import sanbing.jcpp.app.adapter.response.LoginResponse;
|
||||
import sanbing.jcpp.app.service.security.model.SecurityUser;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController extends BaseController {
|
||||
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<ApiResponse<LoginResponse.UserInfo>> getUserInfo() {
|
||||
try {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) {
|
||||
return ResponseEntity.status(401).body(ApiResponse.error(ErrorCode.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
LoginResponse.UserInfo userInfo = LoginResponse.UserInfo.builder()
|
||||
.id(securityUser.getId().toString())
|
||||
.username(securityUser.getUserName())
|
||||
.status(securityUser.isEnabled() ? "ENABLE" : "DISABLE")
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(userInfo));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户信息异常", e);
|
||||
return ResponseEntity.status(500).body(ApiResponse.error("获取用户信息失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* SPA(单页应用)路由控制器
|
||||
* 处理所有前端路由,返回index.html
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Controller
|
||||
public class WebController extends BaseController {
|
||||
|
||||
/**
|
||||
* 处理所有业务页面路由
|
||||
* 统一使用 /page/ 前缀,便于扩展管理
|
||||
*/
|
||||
@GetMapping(value = {"/", "/login", "/page/**"})
|
||||
public String redirect() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class GunCreateRequest {
|
||||
|
||||
@NotBlank(message = "充电枪名称不能为空")
|
||||
@NoXss
|
||||
private String gunName;
|
||||
|
||||
@NotBlank(message = "充电枪编号不能为空")
|
||||
@NoXss
|
||||
private String gunNo;
|
||||
|
||||
@NotBlank(message = "充电枪编码不能为空")
|
||||
@NoXss
|
||||
private String gunCode;
|
||||
|
||||
@NotNull(message = "充电站ID不能为空")
|
||||
private UUID stationId;
|
||||
|
||||
@NotNull(message = "充电桩ID不能为空")
|
||||
private UUID pileId;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GunQueryRequest extends PageRequest {
|
||||
|
||||
private String gunName;
|
||||
|
||||
private String gunNo;
|
||||
|
||||
private String gunCode;
|
||||
|
||||
private UUID stationId;
|
||||
|
||||
private UUID pileId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
@Data
|
||||
public class GunUpdateRequest {
|
||||
|
||||
@NotBlank(message = "充电枪名称不能为空")
|
||||
@NoXss
|
||||
private String gunName;
|
||||
|
||||
private GunRunStatusEnum runStatus;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分页查询请求基类
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class PageRequest {
|
||||
|
||||
private Integer page = 1; // 页码,从1开始
|
||||
private Integer size = 10; // 每页大小
|
||||
private String sortField; // 排序字段
|
||||
private String sortOrder = "desc"; // 排序方向:asc, desc
|
||||
private String search; // 搜索关键词
|
||||
|
||||
/**
|
||||
* 获取MyBatis-Plus的页码(从0开始)
|
||||
*/
|
||||
public long getOffset() {
|
||||
return (long) (page - 1) * size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容方法:获取排序字段
|
||||
*/
|
||||
public String getSortBy() {
|
||||
return sortField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class PileCreateRequest {
|
||||
|
||||
@NotBlank(message = "充电桩名称不能为空")
|
||||
@NoXss
|
||||
private String pileName;
|
||||
|
||||
@NotBlank(message = "充电桩编码不能为空")
|
||||
@NoXss
|
||||
private String pileCode;
|
||||
|
||||
@NotBlank(message = "协议不能为空")
|
||||
@NoXss
|
||||
private String protocol;
|
||||
|
||||
@NotNull(message = "充电站ID不能为空")
|
||||
private UUID stationId;
|
||||
|
||||
@NoXss
|
||||
private String brand;
|
||||
|
||||
@NoXss
|
||||
private String model;
|
||||
|
||||
@NoXss
|
||||
private String manufacturer;
|
||||
|
||||
private PileTypeEnum type = PileTypeEnum.DC;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PileQueryRequest extends PageRequest {
|
||||
|
||||
private String pileName;
|
||||
|
||||
private String pileCode;
|
||||
|
||||
private String protocol;
|
||||
|
||||
private UUID stationId;
|
||||
|
||||
private String brand;
|
||||
|
||||
private String model;
|
||||
|
||||
private String manufacturer;
|
||||
|
||||
private PileTypeEnum type;
|
||||
|
||||
private PileStatusEnum status;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
@Data
|
||||
public class PileUpdateRequest {
|
||||
|
||||
@NotBlank(message = "充电桩名称不能为空")
|
||||
@NoXss
|
||||
private String pileName;
|
||||
|
||||
@NotBlank(message = "协议不能为空")
|
||||
@NoXss
|
||||
private String protocol;
|
||||
|
||||
@NoXss
|
||||
private String brand;
|
||||
|
||||
@NoXss
|
||||
private String model;
|
||||
|
||||
@NoXss
|
||||
private String manufacturer;
|
||||
|
||||
private PileTypeEnum type;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
|
||||
/**
|
||||
* 创建充电站请求
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class StationCreateRequest {
|
||||
|
||||
@NotBlank(message = "充电站名称不能为空")
|
||||
@NoXss
|
||||
private String stationName;
|
||||
|
||||
@NotBlank(message = "充电站编码不能为空")
|
||||
@NoXss
|
||||
private String stationCode;
|
||||
|
||||
private Float longitude; // 经度
|
||||
private Float latitude; // 纬度
|
||||
|
||||
@NoXss
|
||||
private String province; // 省份
|
||||
|
||||
@NoXss
|
||||
private String city; // 城市
|
||||
|
||||
@NoXss
|
||||
private String county; // 区县
|
||||
|
||||
@NoXss
|
||||
private String address; // 详细地址
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
||||
/**
|
||||
* 充电站查询请求
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class StationQueryRequest extends PageRequest {
|
||||
|
||||
private String stationName; // 充电站名称
|
||||
private String stationCode; // 充电站编码
|
||||
private String province; // 省份
|
||||
private String city; // 城市
|
||||
private String county; // 区县
|
||||
private String address; // 详细地址
|
||||
private String keyword; // 关键字搜索(站名或编码)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
|
||||
/**
|
||||
* 更新充电站请求
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class StationUpdateRequest {
|
||||
|
||||
@NotBlank(message = "充电站名称不能为空")
|
||||
@NoXss
|
||||
private String stationName;
|
||||
|
||||
private Float longitude; // 经度
|
||||
private Float latitude; // 纬度
|
||||
|
||||
@NoXss
|
||||
private String province; // 省份
|
||||
|
||||
@NoXss
|
||||
private String city; // 城市
|
||||
|
||||
@NoXss
|
||||
private String county; // 区县
|
||||
|
||||
@NoXss
|
||||
private String address; // 详细地址
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用API响应结果
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private String errorCode;
|
||||
private String message;
|
||||
private T data;
|
||||
private long timestamp;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.message("操作成功")
|
||||
.data(data)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.message(message)
|
||||
.data(data)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String errorCode, String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.errorCode(errorCode)
|
||||
.message(message)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(ErrorCode errorCode, String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.errorCode(errorCode.getCode())
|
||||
.message(message)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(ErrorCode errorCode) {
|
||||
return ApiResponse.<T>builder()
|
||||
.errorCode(errorCode.getCode())
|
||||
.message(errorCode.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return error(ErrorCode.BUSINESS_ERROR, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
/**
|
||||
* 仪表盘统计数据
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DashboardStats {
|
||||
|
||||
/**
|
||||
* 总览统计
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Overview {
|
||||
private Long totalStations; // 总充电站数
|
||||
private Long totalPiles; // 总充电桩数
|
||||
private Long totalGuns; // 总充电枪数
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电桩在线状态分布
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PileStatusDistribution {
|
||||
private Long onlinePiles; // 在线充电桩数
|
||||
private Long offlinePiles; // 离线充电桩数
|
||||
private Long totalPiles; // 总充电桩数
|
||||
|
||||
public double getOnlinePercentage() {
|
||||
return totalPiles > 0 ? (onlinePiles * 100.0) / totalPiles : 0.0;
|
||||
}
|
||||
|
||||
public double getOfflinePercentage() {
|
||||
return totalPiles > 0 ? (offlinePiles * 100.0) / totalPiles : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电枪运行状态分布
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class GunStatusDistribution {
|
||||
private Long idleGuns; // 空闲 (IDLE)
|
||||
private Long insertedGuns; // 已插枪未充电 (INSERTED)
|
||||
private Long chargingGuns; // 充电中 (CHARGING)
|
||||
private Long chargeCompleteGuns; // 充电完成 (CHARGE_COMPLETE)
|
||||
private Long dischargeReadyGuns; // 放电准备 (DISCHARGE_READY)
|
||||
private Long dischargingGuns; // 放电中 (DISCHARGING)
|
||||
private Long dischargeCompleteGuns; // 放电完成 (DISCHARGE_COMPLETE)
|
||||
private Long reservedGuns; // 预约 (RESERVED)
|
||||
private Long faultGuns; // 故障 (FAULT)
|
||||
private Long totalGuns; // 总充电枪数
|
||||
|
||||
public double getIdlePercentage() {
|
||||
return totalGuns > 0 ? (idleGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getInsertedPercentage() {
|
||||
return totalGuns > 0 ? (insertedGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getChargingPercentage() {
|
||||
return totalGuns > 0 ? (chargingGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getChargeCompletePercentage() {
|
||||
return totalGuns > 0 ? (chargeCompleteGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getDischargeReadyPercentage() {
|
||||
return totalGuns > 0 ? (dischargeReadyGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getDischargingPercentage() {
|
||||
return totalGuns > 0 ? (dischargingGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getDischargeCompletePercentage() {
|
||||
return totalGuns > 0 ? (dischargeCompleteGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getReservedPercentage() {
|
||||
return totalGuns > 0 ? (reservedGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
|
||||
public double getFaultPercentage() {
|
||||
return totalGuns > 0 ? (faultGuns * 100.0) / totalGuns : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
private Overview overview;
|
||||
private PileStatusDistribution pileStatusDistribution;
|
||||
private GunStatusDistribution gunStatusDistribution;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
/**
|
||||
* 统一错误码管理
|
||||
* 避免魔法值硬编码,便于维护和扩展
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum ErrorCode {
|
||||
|
||||
// ==================== 通用错误码 ====================
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
SUCCESS("SUCCESS", "操作成功"),
|
||||
|
||||
/**
|
||||
* 系统异常
|
||||
*/
|
||||
SYSTEM_ERROR("SYSTEM_ERROR", "系统异常,请稍后重试"),
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
BUSINESS_ERROR("BUSINESS_ERROR", "业务处理失败"),
|
||||
|
||||
// ==================== 参数校验相关 ====================
|
||||
/**
|
||||
* 参数校验失败
|
||||
*/
|
||||
VALIDATION_ERROR("VALIDATION_ERROR", "参数校验失败"),
|
||||
|
||||
/**
|
||||
* 数据绑定异常
|
||||
*/
|
||||
BINDING_ERROR("BINDING_ERROR", "数据绑定异常"),
|
||||
|
||||
/**
|
||||
* 非法参数
|
||||
*/
|
||||
ILLEGAL_ARGUMENT("ILLEGAL_ARGUMENT", "参数错误"),
|
||||
|
||||
/**
|
||||
* 非法状态
|
||||
*/
|
||||
ILLEGAL_STATE("ILLEGAL_STATE", "状态错误"),
|
||||
|
||||
// ==================== 认证授权相关 ====================
|
||||
/**
|
||||
* 未认证
|
||||
*/
|
||||
UNAUTHORIZED("UNAUTHORIZED", "用户未认证"),
|
||||
|
||||
/**
|
||||
* 认证失败
|
||||
*/
|
||||
AUTH_FAILED("AUTH_FAILED", "用户名或密码错误"),
|
||||
|
||||
/**
|
||||
* JWT认证失败
|
||||
*/
|
||||
JWT_AUTH_FAILED("JWT_AUTH_FAILED", "JWT Token认证失败"),
|
||||
|
||||
/**
|
||||
* 权限不足
|
||||
*/
|
||||
FORBIDDEN("FORBIDDEN", "权限不足"),
|
||||
|
||||
// ==================== 资源相关 ====================
|
||||
/**
|
||||
* 资源不存在
|
||||
*/
|
||||
NOT_FOUND("NOT_FOUND", "请求的资源不存在"),
|
||||
|
||||
/**
|
||||
* 资源冲突
|
||||
*/
|
||||
CONFLICT("CONFLICT", "资源冲突"),
|
||||
|
||||
// ==================== 业务特定错误码 ====================
|
||||
/**
|
||||
* 充电桩编码已存在
|
||||
*/
|
||||
PILE_CODE_EXISTS("PILE_CODE_EXISTS", "充电桩编码已存在"),
|
||||
|
||||
/**
|
||||
* 充电站名称已存在
|
||||
*/
|
||||
STATION_NAME_EXISTS("STATION_NAME_EXISTS", "充电站名称已存在"),
|
||||
|
||||
/**
|
||||
* 充电枪编号已存在
|
||||
*/
|
||||
GUN_CODE_EXISTS("GUN_CODE_EXISTS", "充电枪编号已存在"),
|
||||
|
||||
/**
|
||||
* 充电桩不存在
|
||||
*/
|
||||
PILE_NOT_FOUND("PILE_NOT_FOUND", "充电桩不存在"),
|
||||
|
||||
/**
|
||||
* 充电站不存在
|
||||
*/
|
||||
STATION_NOT_FOUND("STATION_NOT_FOUND", "充电站不存在"),
|
||||
|
||||
/**
|
||||
* 充电枪不存在
|
||||
*/
|
||||
GUN_NOT_FOUND("GUN_NOT_FOUND", "充电枪不存在");
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
|
||||
ErrorCode(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误码查找枚举
|
||||
*
|
||||
* @param code 错误码
|
||||
* @return 对应的枚举,如果不存在返回null
|
||||
*/
|
||||
public static ErrorCode fromCode(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (ErrorCode errorCode : ErrorCode.values()) {
|
||||
if (errorCode.getCode().equals(code)) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 充电枪响应DTO(包含状态信息)
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class GunWithStatusResponse {
|
||||
|
||||
/**
|
||||
* 充电枪ID
|
||||
*/
|
||||
private UUID id;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
/**
|
||||
* 充电枪名称
|
||||
*/
|
||||
private String gunName;
|
||||
|
||||
/**
|
||||
* 充电枪编号,不允许修改
|
||||
*/
|
||||
private String gunNo;
|
||||
|
||||
/**
|
||||
* 充电枪编码,不允许修改
|
||||
*/
|
||||
private String gunCode;
|
||||
|
||||
/**
|
||||
* 充电站ID
|
||||
*/
|
||||
private UUID stationId;
|
||||
|
||||
/**
|
||||
* 充电桩ID
|
||||
*/
|
||||
private UUID pileId;
|
||||
|
||||
/**
|
||||
* 充电站名称
|
||||
*/
|
||||
private String stationName;
|
||||
|
||||
/**
|
||||
* 充电桩名称
|
||||
*/
|
||||
private String pileName;
|
||||
|
||||
/**
|
||||
* 充电桩编码
|
||||
*/
|
||||
private String pileCode;
|
||||
|
||||
/**
|
||||
* 附加信息
|
||||
*/
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
// ========== 状态信息 ==========
|
||||
|
||||
/**
|
||||
* 充电枪运行状态
|
||||
*/
|
||||
private GunRunStatusEnum runStatus;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 登录响应DTO
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
private String token;
|
||||
private String refreshToken;
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
private UserInfo user;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UserInfo {
|
||||
private String id;
|
||||
private String username;
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageResponse<T> {
|
||||
|
||||
private List<T> records; // 数据列表
|
||||
private Long total; // 总记录数
|
||||
private Integer page; // 当前页码
|
||||
private Integer size; // 每页大小
|
||||
private Integer totalPages; // 总页数
|
||||
|
||||
public static <T> PageResponse<T> of(List<T> records, Long total, Integer page, Integer size) {
|
||||
int totalPages = (int) Math.ceil((double) total / size);
|
||||
return PageResponse.<T>builder()
|
||||
.records(records)
|
||||
.total(total)
|
||||
.page(page)
|
||||
.size(size)
|
||||
.totalPages(totalPages)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PileOptionResponse {
|
||||
|
||||
private UUID id;
|
||||
|
||||
private String label; // 显示名称,格式:pileName (pileCode)
|
||||
|
||||
private String pileName;
|
||||
|
||||
private String pileCode;
|
||||
|
||||
private UUID stationId;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 充电桩响应DTO(包含状态信息)
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class PileWithStatusResponse {
|
||||
|
||||
/**
|
||||
* 充电桩ID
|
||||
*/
|
||||
private UUID id;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
/**
|
||||
* 充电桩名称
|
||||
*/
|
||||
private String pileName;
|
||||
|
||||
/**
|
||||
* 充电桩编码,不允许修改
|
||||
*/
|
||||
private String pileCode;
|
||||
|
||||
/**
|
||||
* 协议类型
|
||||
*/
|
||||
private String protocol;
|
||||
|
||||
/**
|
||||
* 充电站ID
|
||||
*/
|
||||
private UUID stationId;
|
||||
|
||||
/**
|
||||
* 品牌
|
||||
*/
|
||||
private String brand;
|
||||
|
||||
/**
|
||||
* 型号
|
||||
*/
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 制造商
|
||||
*/
|
||||
private String manufacturer;
|
||||
|
||||
/**
|
||||
* 充电桩类型(交流/直流)
|
||||
*/
|
||||
private PileTypeEnum type;
|
||||
|
||||
/**
|
||||
* 附加信息
|
||||
*/
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
// ========== 状态信息 ==========
|
||||
|
||||
/**
|
||||
* 充电桩状态
|
||||
*/
|
||||
private PileStatusEnum status;
|
||||
|
||||
/**
|
||||
* 最近连接时间(13位时间戳)
|
||||
*/
|
||||
private Long connectedAt;
|
||||
|
||||
/**
|
||||
* 最后断线时间(13位时间戳)
|
||||
*/
|
||||
private Long disconnectedAt;
|
||||
|
||||
/**
|
||||
* 最后活跃时间(13位时间戳)
|
||||
*/
|
||||
private Long lastActiveTime;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.protocol.enums.SupportedProtocols;
|
||||
|
||||
/**
|
||||
* 协议选项响应
|
||||
* 用于前端下拉选择组件
|
||||
*
|
||||
* @author 九筒
|
||||
* @since 2024-12-22
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProtocolOption {
|
||||
|
||||
private String value; // 协议标识符(用于表单提交)
|
||||
private String label; // 显示名称(用于前端显示)
|
||||
|
||||
/**
|
||||
* 从协议信息创建选项
|
||||
* @param protocolInfo 协议信息
|
||||
* @return 协议选项
|
||||
*/
|
||||
public static ProtocolOption fromProtocolInfo(SupportedProtocols.ProtocolInfo protocolInfo) {
|
||||
return new ProtocolOption(
|
||||
protocolInfo.protocolId(),
|
||||
protocolInfo.displayName()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.adapter.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 充电站选项响应
|
||||
* 用于下拉选择组件
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class StationOption {
|
||||
|
||||
private UUID id; // 充电站ID
|
||||
private String label; // 显示名称:stationName (stationCode)
|
||||
private String stationName; // 充电站名称
|
||||
private String stationCode; // 充电站编码
|
||||
|
||||
public static StationOption of(UUID id, String stationName, String stationCode) {
|
||||
StationOption option = new StationOption();
|
||||
option.setId(id);
|
||||
option.setStationName(stationName);
|
||||
option.setStationCode(stationCode);
|
||||
option.setLabel(stationName + " (" + stationCode + ")");
|
||||
return option;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* 用户权限枚举
|
||||
* 对应 sanbing.jcpp.app.service.security.model.Authority
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum AuthorityEnum implements IEnum<String> {
|
||||
|
||||
/**
|
||||
* 系统管理员
|
||||
*/
|
||||
SYS_ADMIN,
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
REFRESH_TOKEN,
|
||||
;
|
||||
|
||||
|
||||
public static AuthorityEnum parse(String value) {
|
||||
AuthorityEnum authority = null;
|
||||
if (value != null && !value.isEmpty()) {
|
||||
for (AuthorityEnum current : AuthorityEnum.values()) {
|
||||
if (current.name().equalsIgnoreCase(value)) {
|
||||
authority = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return authority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum GunOptStatusEnum implements IEnum<String> {
|
||||
AVAILABLE, // 可用状态
|
||||
IN_MAINTENANCE, // 维护中状态
|
||||
OUT_OF_SERVICE, // 停用状态
|
||||
RESERVED; // 已预约状态
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,17 @@ package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum GunRunStatusEnum implements IEnum<String> {
|
||||
IDLE, // 空闲
|
||||
INSERTED, // 已插枪
|
||||
CHARGING, // 充电中
|
||||
CHARGE_COMPLETE, // 充电完成
|
||||
INSERTED, // 已插枪 占用(未充电)
|
||||
CHARGING, // 充电中 占用(充电中)
|
||||
CHARGE_COMPLETE, // 充电完成 占用(预约锁定)
|
||||
DISCHARGE_READY, // 放电准备
|
||||
DISCHARGING, // 放电中
|
||||
DISCHARGE_COMPLETE, // 放电完成
|
||||
RESERVED, // 预约
|
||||
RESERVED, // 预约 占用(预约锁定)
|
||||
FAULT; // 故障
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
public enum OrderStatusEnum implements IEnum<String> {
|
||||
PENDING,
|
||||
IN_CHARGING,
|
||||
COMPLETED,
|
||||
CANCELLED,
|
||||
TERMINATED,
|
||||
FAILED,
|
||||
REFUNDED;
|
||||
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return name();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum OrderTypeEnum implements IEnum<String> {
|
||||
CHARGE,
|
||||
|
||||
DISCHARGE;
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum OwnerTypeEnum implements IEnum<String> {
|
||||
C,
|
||||
B,
|
||||
G;
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,39 @@ package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* 充电桩状态枚举 - 简化版本,只维护在线/离线状态
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* - 充电桩状态独立于充电枪状态,不受枪的工作状态影响
|
||||
* - 只关注设备的网络连接状态和基础可用性
|
||||
* - 充电枪的具体工作状态通过GunRunStatusEnum单独维护
|
||||
* <p>
|
||||
* 状态转换场景:
|
||||
* 1. 设备登录成功 → ONLINE
|
||||
* 2. 设备心跳正常 → 保持ONLINE
|
||||
* 3. 设备断开连接 → OFFLINE
|
||||
* 4. 设备超时无响应 → OFFLINE
|
||||
* 5. 系统重启后清洗 → 根据连接状态决定ONLINE/OFFLINE
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum PileStatusEnum implements IEnum<String> {
|
||||
IDLE, // 空闲
|
||||
WORKING, // 工作中
|
||||
FAULT, // 故障
|
||||
MAINTENANCE, // 维护中
|
||||
OFFLINE, // 离线
|
||||
/**
|
||||
* 在线状态:设备已连接并能正常通信
|
||||
* - 设备登录成功
|
||||
* - 心跳正常
|
||||
* - 能接收和响应指令
|
||||
*/
|
||||
ONLINE,
|
||||
|
||||
/**
|
||||
* 离线状态:设备未连接或无法通信
|
||||
* - 设备未登录
|
||||
* - 网络连接断开
|
||||
* - 心跳超时
|
||||
* - 系统重启后未重新连接
|
||||
*/
|
||||
OFFLINE,
|
||||
;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,7 +9,7 @@ package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum PileTypeEnum implements IEnum<String> {
|
||||
AC, // 交流充电桩
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum StationStatusEnum implements IEnum<String> {
|
||||
OPERATIONAL, // 正常运营
|
||||
PARTIAL_FAILURE, // 部分故障
|
||||
FULLY_LOADED, // 满载
|
||||
MAINTENANCE, // 维护中
|
||||
CLOSED, // 关闭
|
||||
WAITING_FOR_OPEN; // 待开放
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return name();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -9,7 +9,7 @@ package sanbing.jcpp.app.dal.config.ibatis.enums;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum UserStatusEnum implements IEnum<String> {
|
||||
ENABLE,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.config.ibatis.typehandlers;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.postgresql.util.PGobject;
|
||||
import sanbing.jcpp.app.service.security.model.UserCredentials;
|
||||
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* UserCredentials 类型处理器
|
||||
* 负责 PostgreSQL JSONB 和 UserCredentials 对象之间的转换
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Slf4j
|
||||
@MappedTypes({UserCredentials.class})
|
||||
public class UserCredentialsTypeHandler extends BaseTypeHandler<UserCredentials> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, UserCredentials parameter, JdbcType jdbcType) throws SQLException {
|
||||
if (ps == null) {
|
||||
throw new SQLException("PreparedStatement cannot be null");
|
||||
}
|
||||
|
||||
if (parameter != null) {
|
||||
try {
|
||||
PGobject jsonObject = new PGobject();
|
||||
jsonObject.setType("jsonb");
|
||||
jsonObject.setValue(JacksonUtil.toString(parameter));
|
||||
ps.setObject(i, jsonObject);
|
||||
log.debug("Set UserCredentials parameter at index {}: failedLoginAttempts={}", i, parameter.getFailedLoginAttempts());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to serialize UserCredentials to JSONB", e);
|
||||
throw new SQLException("Failed to serialize UserCredentials", e);
|
||||
}
|
||||
} else {
|
||||
ps.setNull(i, java.sql.Types.OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return parseUserCredentials(rs.getString(columnName), columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return parseUserCredentials(rs.getString(columnIndex), "column_" + columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return parseUserCredentials(cs.getString(columnIndex), "column_" + columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串为 UserCredentials 对象
|
||||
*/
|
||||
private UserCredentials parseUserCredentials(String jsonString, String columnIdentifier) {
|
||||
if (jsonString == null || jsonString.trim().isEmpty()) {
|
||||
log.debug("UserCredentials JSON is null or empty for {}", columnIdentifier);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
UserCredentials userCredentials = JacksonUtil.fromString(jsonString, UserCredentials.class);
|
||||
if (userCredentials != null) {
|
||||
log.debug("Parsed UserCredentials from {}: failedLoginAttempts={}",
|
||||
columnIdentifier, userCredentials.getFailedLoginAttempts());
|
||||
}
|
||||
return userCredentials;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse UserCredentials from JSON: {} for {}", jsonString, columnIdentifier, e);
|
||||
// 返回 null 而不是抛出异常,避免查询失败
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import sanbing.jcpp.app.data.kv.*;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 属性实体,用于存储设备的最新属性数据
|
||||
* 采用键值对存储结构设计
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_attr")
|
||||
public class Attribute implements Serializable, HasVersion {
|
||||
|
||||
/**
|
||||
* 实体ID (UUID保证全局唯一)
|
||||
* 复合主键的一部分
|
||||
*/
|
||||
@TableId(value = "entity_id", type = IdType.INPUT)
|
||||
private UUID entityId;
|
||||
|
||||
/**
|
||||
* 属性键 (字符串类型提高可读性)
|
||||
* 复合主键的一部分
|
||||
*/
|
||||
@TableField("attr_key")
|
||||
private String attrKey;
|
||||
|
||||
/**
|
||||
* 布尔值
|
||||
*/
|
||||
@TableField("bool_v")
|
||||
private Boolean boolV;
|
||||
|
||||
/**
|
||||
* 字符串值
|
||||
*/
|
||||
@TableField("str_v")
|
||||
private String strV;
|
||||
|
||||
/**
|
||||
* 长整型值
|
||||
*/
|
||||
@TableField("long_v")
|
||||
private Long longV;
|
||||
|
||||
/**
|
||||
* 双精度值
|
||||
*/
|
||||
@TableField("dbl_v")
|
||||
private Double dblV;
|
||||
|
||||
/**
|
||||
* JSON值
|
||||
*/
|
||||
@TableField("json_v")
|
||||
private String jsonV;
|
||||
|
||||
/**
|
||||
* 最后更新时间戳
|
||||
*/
|
||||
@TableField("last_update_ts")
|
||||
private Long lastUpdateTs;
|
||||
|
||||
/**
|
||||
* 版本号(用于乐观锁控制)
|
||||
*/
|
||||
@TableField
|
||||
private Integer version;
|
||||
|
||||
public AttributeKvEntry toData() {
|
||||
KvEntry kvEntry = null;
|
||||
if (strV != null) {
|
||||
kvEntry = new StringDataEntry(attrKey, strV);
|
||||
} else if (boolV != null) {
|
||||
kvEntry = new BooleanDataEntry(attrKey, boolV);
|
||||
} else if (dblV != null) {
|
||||
kvEntry = new DoubleDataEntry(attrKey, dblV);
|
||||
} else if (longV != null) {
|
||||
kvEntry = new LongDataEntry(attrKey, longV);
|
||||
} else if (jsonV != null) {
|
||||
kvEntry = new JsonDataEntry(attrKey, jsonV);
|
||||
}
|
||||
|
||||
return new BaseAttributeKvEntry(kvEntry, lastUpdateTs, version);
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.GunOptStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -25,7 +23,7 @@ import java.util.UUID;
|
||||
|
||||
|
||||
@Data
|
||||
@TableName("jcpp_gun")
|
||||
@TableName("t_gun")
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -36,28 +34,23 @@ public class Gun implements Serializable, HasVersion {
|
||||
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
@NoXss
|
||||
private String gunNo;
|
||||
|
||||
@NoXss
|
||||
private String gunName;
|
||||
|
||||
@NoXss
|
||||
private String gunCode;
|
||||
|
||||
private UUID stationId;
|
||||
|
||||
private UUID pileId;
|
||||
|
||||
private UUID ownerId;
|
||||
|
||||
private OwnerTypeEnum ownerType;
|
||||
|
||||
private GunRunStatusEnum runStatus;
|
||||
|
||||
private LocalDateTime runStatusUpdatedTime;
|
||||
|
||||
private GunOptStatusEnum optStatus;
|
||||
|
||||
private Integer version;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.OrderStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.OrderTypeEnum;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
@Data
|
||||
@TableName("jcpp_order")
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Order implements Serializable {
|
||||
|
||||
@TableId(type = IdType.INPUT)
|
||||
private UUID id;
|
||||
|
||||
private String internalOrderNo;
|
||||
|
||||
private String externalOrderNo;
|
||||
|
||||
private String pileOrderNo;
|
||||
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private LocalDateTime cancelledTime;
|
||||
|
||||
private OrderStatusEnum status;
|
||||
|
||||
private OrderTypeEnum type;
|
||||
|
||||
private UUID creatorId;
|
||||
|
||||
private UUID stationId;
|
||||
|
||||
private UUID pileId;
|
||||
|
||||
private UUID gunId;
|
||||
|
||||
private String plateNo;
|
||||
|
||||
private BigDecimal settlementAmount;
|
||||
|
||||
private JsonNode settlementDetails;
|
||||
|
||||
private BigDecimal electricityQuantity;
|
||||
|
||||
|
||||
}
|
||||
@@ -14,17 +14,16 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@TableName(value = "jcpp_pile", autoResultMap = true)
|
||||
@TableName(value = "t_pile", autoResultMap = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -35,28 +34,30 @@ public class Pile implements Serializable, HasVersion {
|
||||
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
@NoXss
|
||||
private String pileName;
|
||||
|
||||
@NoXss
|
||||
private String pileCode;
|
||||
|
||||
@NoXss
|
||||
private String protocol;
|
||||
|
||||
private UUID stationId;
|
||||
|
||||
private UUID ownerId;
|
||||
|
||||
private OwnerTypeEnum ownerType;
|
||||
|
||||
@NoXss
|
||||
private String brand;
|
||||
|
||||
@NoXss
|
||||
private String model;
|
||||
|
||||
@NoXss
|
||||
private String manufacturer;
|
||||
|
||||
private PileStatusEnum status;
|
||||
|
||||
private PileTypeEnum type;
|
||||
|
||||
private Integer version;
|
||||
|
||||
@@ -14,9 +14,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.StationStatusEnum;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -24,7 +23,7 @@ import java.util.UUID;
|
||||
|
||||
|
||||
@Data
|
||||
@TableName("jcpp_station")
|
||||
@TableName("t_station")
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -35,30 +34,32 @@ public class Station implements Serializable, HasVersion {
|
||||
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
@NoXss
|
||||
private String stationName;
|
||||
|
||||
@NoXss
|
||||
private String stationCode;
|
||||
|
||||
private UUID ownerId;
|
||||
|
||||
private Float longitude;
|
||||
|
||||
private Float latitude;
|
||||
|
||||
private OwnerTypeEnum ownerType;
|
||||
|
||||
@NoXss
|
||||
private String province;
|
||||
|
||||
@NoXss
|
||||
private String city;
|
||||
|
||||
@NoXss
|
||||
private String county;
|
||||
|
||||
@NoXss
|
||||
private String address;
|
||||
|
||||
private StationStatusEnum status;
|
||||
|
||||
private Integer version;
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package sanbing.jcpp.app.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -14,7 +15,10 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.AuthorityEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.UserStatusEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.typehandlers.UserCredentialsTypeHandler;
|
||||
import sanbing.jcpp.app.service.security.model.UserCredentials;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -23,24 +27,45 @@ import java.util.UUID;
|
||||
|
||||
|
||||
@Data
|
||||
@TableName("jcpp_user")
|
||||
@TableName("t_user")
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class User implements Serializable, HasVersion {
|
||||
|
||||
public User(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User(User user) {
|
||||
this.id = user.getId();
|
||||
this.createdTime = user.getCreatedTime();
|
||||
this.updatedTime = user.getUpdatedTime();
|
||||
this.additionalInfo = user.getAdditionalInfo();
|
||||
this.status = user.getStatus();
|
||||
this.userName = user.getUserName();
|
||||
this.userCredentials = user.getUserCredentials();
|
||||
this.authority = user.getAuthority();
|
||||
this.version = user.getVersion();
|
||||
}
|
||||
|
||||
@TableId(type = IdType.INPUT)
|
||||
private UUID id;
|
||||
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private JsonNode additionalInfo;
|
||||
|
||||
private UserStatusEnum status;
|
||||
|
||||
private String userName;
|
||||
|
||||
private JsonNode userCredentials;
|
||||
@TableField(typeHandler = UserCredentialsTypeHandler.class)
|
||||
private UserCredentials userCredentials;
|
||||
|
||||
private AuthorityEnum authority;
|
||||
|
||||
private Integer version;
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import sanbing.jcpp.app.dal.entity.Attribute;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 属性数据访问层
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Mapper
|
||||
public interface AttributeMapper extends BaseMapper<Attribute> {
|
||||
|
||||
/**
|
||||
* 查询实体的所有属性
|
||||
*/
|
||||
List<Attribute> findByEntity(@Param("entityId") UUID entityId);
|
||||
|
||||
/**
|
||||
* 查询实体的特定属性
|
||||
*/
|
||||
Attribute findByEntityAndKey(@Param("entityId") UUID entityId, @Param("attrKey") String attrKey);
|
||||
|
||||
/**
|
||||
* 查询实体在指定属性类型下的所有属性 (兼容原JPA方法)
|
||||
* 注意:此方法主要用于兼容性,实际t_attr表中没有attribute_type字段
|
||||
*/
|
||||
List<Attribute> findAllByEntityIdAndAttributeType(@Param("entityId") UUID entityId);
|
||||
|
||||
/**
|
||||
* 删除指定实体的指定属性
|
||||
*/
|
||||
void deleteByEntityIdAndKey(@Param("entityId") UUID entityId,
|
||||
@Param("attrKey") String attrKey);
|
||||
|
||||
/**
|
||||
* 根据实体ID和属性键列表查询属性
|
||||
*
|
||||
*/
|
||||
List<Attribute> findAllByIdAndAttrKey(UUID entityId, Collection<String> attrKeys);
|
||||
}
|
||||
@@ -7,10 +7,117 @@
|
||||
package sanbing.jcpp.app.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import sanbing.jcpp.app.adapter.request.GunQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.response.GunWithStatusResponse;
|
||||
import sanbing.jcpp.app.dal.entity.Gun;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface GunMapper extends BaseMapper<Gun> {
|
||||
|
||||
/**
|
||||
* 根据充电桩编码和充电枪编码查询充电枪
|
||||
*/
|
||||
Gun selectByPileCodeAndGunCode(@Param("pileCode") String pileCode, @Param("gunCode") String gunCode);
|
||||
|
||||
/**
|
||||
* 分页查询充电枪及其状态信息
|
||||
* 使用MyBatis XML配置,避免魔法值错误,提高SQL可读性和可维护性
|
||||
*/
|
||||
IPage<GunWithStatusResponse> selectGunWithStatusPage(Page<GunWithStatusResponse> page, @Param("request") GunQueryRequest request);
|
||||
|
||||
/**
|
||||
* 统计充电桩下的充电枪数量
|
||||
*
|
||||
* @param pileId 充电桩ID
|
||||
* @return 充电枪数量
|
||||
*/
|
||||
long countByPileId(@Param("pileId") UUID pileId);
|
||||
|
||||
/**
|
||||
* 统计空闲状态的充电枪数量 (IDLE)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 空闲充电枪数量
|
||||
*/
|
||||
long countIdleGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计已插枪未充电状态的充电枪数量 (INSERTED)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 已插枪未充电充电枪数量
|
||||
*/
|
||||
long countInsertedGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计充电中状态的充电枪数量 (CHARGING)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 充电中充电枪数量
|
||||
*/
|
||||
long countChargingGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计充电完成状态的充电枪数量 (CHARGE_COMPLETE)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 充电完成充电枪数量
|
||||
*/
|
||||
long countChargeCompleteGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计放电准备状态的充电枪数量 (DISCHARGE_READY)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 放电准备充电枪数量
|
||||
*/
|
||||
long countDischargeReadyGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计放电中状态的充电枪数量 (DISCHARGING)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 放电中充电枪数量
|
||||
*/
|
||||
long countDischargingGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计放电完成状态的充电枪数量 (DISCHARGE_COMPLETE)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 放电完成充电枪数量
|
||||
*/
|
||||
long countDischargeCompleteGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计预约状态的充电枪数量 (RESERVED)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 预约充电枪数量
|
||||
*/
|
||||
long countReservedGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 统计故障状态的充电枪数量 (FAULT)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param status 状态值
|
||||
* @return 故障充电枪数量
|
||||
*/
|
||||
long countFaultGuns(@Param("statusKey") String statusKey, @Param("status") String status);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import sanbing.jcpp.app.dal.entity.Order;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public interface OrderMapper extends BaseMapper<Order> {
|
||||
}
|
||||
@@ -7,19 +7,72 @@
|
||||
package sanbing.jcpp.app.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import sanbing.jcpp.app.adapter.request.PileQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.response.PileWithStatusResponse;
|
||||
import sanbing.jcpp.app.dal.entity.Pile;
|
||||
import sanbing.jcpp.app.data.kv.AttrKeyEnum;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface PileMapper extends BaseMapper<Pile> {
|
||||
|
||||
@Select("SELECT " +
|
||||
" * " +
|
||||
"FROM " +
|
||||
" jcpp_pile " +
|
||||
"WHERE " +
|
||||
" pile_code = #{pileCode}")
|
||||
/**
|
||||
* 根据充电桩编码查询充电桩
|
||||
*
|
||||
* @param pileCode 充电桩编码
|
||||
* @return 充电桩实体
|
||||
*/
|
||||
Pile selectByCode(String pileCode);
|
||||
|
||||
/**
|
||||
* 分页查询充电桩及其状态信息
|
||||
* 使用MyBatis XML配置,避免魔法值错误,提高SQL可读性和可维护性
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param request 查询请求参数
|
||||
* @param statusKey 状态属性键
|
||||
* @param connectedAtKey 连接时间属性键
|
||||
* @param disconnectedAtKey 断开连接时间属性键
|
||||
* @param lastActiveTimeKey 最后活跃时间属性键
|
||||
*/
|
||||
IPage<PileWithStatusResponse> selectPileWithStatusPage(
|
||||
Page<PileWithStatusResponse> page,
|
||||
@Param("request") PileQueryRequest request,
|
||||
@Param("statusKey") AttrKeyEnum statusKey,
|
||||
@Param("connectedAtKey") AttrKeyEnum connectedAtKey,
|
||||
@Param("disconnectedAtKey") AttrKeyEnum disconnectedAtKey,
|
||||
@Param("lastActiveTimeKey") AttrKeyEnum lastActiveTimeKey
|
||||
);
|
||||
|
||||
/**
|
||||
* 统计充电站下的充电桩数量
|
||||
*
|
||||
* @param stationId 充电站ID
|
||||
* @return 充电桩数量
|
||||
*/
|
||||
long countByStationId(@Param("stationId") UUID stationId);
|
||||
|
||||
/**
|
||||
* 统计在线充电桩数量
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param onlineStatus 在线状态值
|
||||
* @return 在线充电桩数量
|
||||
*/
|
||||
long countOnlinePiles(@Param("statusKey") String statusKey, @Param("onlineStatus") String onlineStatus);
|
||||
|
||||
/**
|
||||
* 统计离线充电桩数量(包括未设置状态的)
|
||||
*
|
||||
* @param statusKey 状态属性键
|
||||
* @param offlineStatus 离线状态值
|
||||
* @return 离线充电桩数量
|
||||
*/
|
||||
long countOfflinePiles(@Param("statusKey") String statusKey, @Param("offlineStatus") String offlineStatus);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import sanbing.jcpp.app.dal.entity.Station;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface StationMapper extends BaseMapper<Station> {
|
||||
}
|
||||
@@ -7,10 +7,24 @@
|
||||
package sanbing.jcpp.app.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import sanbing.jcpp.app.dal.entity.User;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户(默认不区分大小写)
|
||||
*/
|
||||
@Select("SELECT * FROM t_user WHERE LOWER(user_name) = LOWER(#{userName})")
|
||||
User findByUserName(@Param("userName") String userName);
|
||||
|
||||
/**
|
||||
* 检查用户名是否已存在(默认不区分大小写)
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM t_user WHERE LOWER(user_name) = LOWER(#{userName})")
|
||||
int countByUserName(@Param("userName") String userName);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository;
|
||||
|
||||
import sanbing.jcpp.app.dal.entity.Gun;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 充电枪数据访问接口
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface GunRepository {
|
||||
|
||||
/**
|
||||
* 根据充电桩编码和充电枪编码查询充电枪
|
||||
*
|
||||
* @param pileCode 充电桩编码
|
||||
* @param gunCode 充电枪编码
|
||||
* @return 充电枪实体,如果不存在返回null
|
||||
*/
|
||||
Gun findByPileCodeAndGunCode(String pileCode, String gunCode);
|
||||
|
||||
/**
|
||||
* 根据充电枪ID查询充电枪
|
||||
*
|
||||
* @param gunId 充电枪ID
|
||||
* @return 充电枪实体,如果不存在返回null
|
||||
*/
|
||||
Gun findById(UUID gunId);
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.repository;
|
||||
package sanbing.jcpp.app.dal.repository;
|
||||
|
||||
import sanbing.jcpp.app.dal.entity.Pile;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface PileRepository {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.attribute;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.PreparedStatementCreator;
|
||||
import org.springframework.jdbc.core.SqlProvider;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import sanbing.jcpp.app.dal.entity.Attribute;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Repository
|
||||
public class AttributeKvInsertRepository {
|
||||
|
||||
private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
|
||||
private static final String EMPTY_STR = "";
|
||||
|
||||
@Value("${sql.remove_null_chars:true}")
|
||||
private boolean removeNullChars;
|
||||
|
||||
@Resource
|
||||
protected JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Resource
|
||||
protected TransactionTemplate transactionTemplate;
|
||||
|
||||
private static final String BATCH_UPDATE = "UPDATE t_attr SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attr_kv_version_seq') " +
|
||||
"WHERE entity_id = ? and attr_key = ? RETURNING version;";
|
||||
|
||||
private static final String INSERT_OR_UPDATE =
|
||||
"INSERT INTO t_attr (entity_id, attr_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, version) " +
|
||||
"VALUES(?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attr_kv_version_seq')) " +
|
||||
"ON CONFLICT (entity_id, attr_key) " +
|
||||
"DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attr_kv_version_seq') RETURNING version;";
|
||||
|
||||
// 合并自 AbstractInsertRepository 的方法
|
||||
protected String replaceNullChars(String strValue) {
|
||||
if (removeNullChars && strValue != null) {
|
||||
return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR);
|
||||
}
|
||||
return strValue;
|
||||
}
|
||||
|
||||
// 合并自 AbstractVersionedInsertRepository 的方法
|
||||
public List<Integer> saveOrUpdate(List<Attribute> entities) {
|
||||
return transactionTemplate.execute(status -> {
|
||||
List<Integer> seqNumbers = new ArrayList<>(entities.size());
|
||||
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
|
||||
int[] updateResult = onBatchUpdate(entities, keyHolder);
|
||||
|
||||
List<Map<String, Object>> seqNumbersList = keyHolder.getKeyList();
|
||||
|
||||
int notUpdatedCount = entities.size() - seqNumbersList.size();
|
||||
|
||||
List<Integer> toInsertIndexes = new ArrayList<>(notUpdatedCount);
|
||||
List<Attribute> insertEntities = new ArrayList<>(notUpdatedCount);
|
||||
for (int i = 0, keyHolderIndex = 0; i < updateResult.length; i++) {
|
||||
if (updateResult[i] == 0) {
|
||||
insertEntities.add(entities.get(i));
|
||||
seqNumbers.add(null);
|
||||
toInsertIndexes.add(i);
|
||||
} else {
|
||||
seqNumbers.add((Integer) seqNumbersList.get(keyHolderIndex).get("version"));
|
||||
keyHolderIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertEntities.isEmpty()) {
|
||||
return seqNumbers;
|
||||
}
|
||||
|
||||
int[] insertResult = onInsertOrUpdate(insertEntities, keyHolder);
|
||||
|
||||
seqNumbersList = keyHolder.getKeyList();
|
||||
|
||||
for (int i = 0, keyHolderIndex = 0; i < insertResult.length; i++) {
|
||||
if (insertResult[i] != 0) {
|
||||
seqNumbers.set(toInsertIndexes.get(i), (Integer) seqNumbersList.get(keyHolderIndex).get("version"));
|
||||
keyHolderIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return seqNumbers;
|
||||
});
|
||||
}
|
||||
|
||||
private int[] onBatchUpdate(List<Attribute> entities, KeyHolder keyHolder) {
|
||||
return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getBatchUpdateQuery()), new BatchPreparedStatementSetter() {
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
setOnBatchUpdateValues(ps, i, entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return entities.size();
|
||||
}
|
||||
}, keyHolder);
|
||||
}
|
||||
|
||||
private int[] onInsertOrUpdate(List<Attribute> insertEntities, KeyHolder keyHolder) {
|
||||
return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getInsertOrUpdateQuery()), new BatchPreparedStatementSetter() {
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
setOnInsertOrUpdateValues(ps, i, insertEntities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return insertEntities.size();
|
||||
}
|
||||
}, keyHolder);
|
||||
}
|
||||
|
||||
protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List<Attribute> entities) throws SQLException {
|
||||
Attribute kvEntity = entities.get(i);
|
||||
ps.setString(1, replaceNullChars(kvEntity.getStrV()));
|
||||
|
||||
if (kvEntity.getLongV() != null) {
|
||||
ps.setLong(2, kvEntity.getLongV());
|
||||
} else {
|
||||
ps.setNull(2, Types.BIGINT);
|
||||
}
|
||||
|
||||
if (kvEntity.getDblV() != null) {
|
||||
ps.setDouble(3, kvEntity.getDblV());
|
||||
} else {
|
||||
ps.setNull(3, Types.DOUBLE);
|
||||
}
|
||||
|
||||
if (kvEntity.getBoolV() != null) {
|
||||
ps.setBoolean(4, kvEntity.getBoolV());
|
||||
} else {
|
||||
ps.setNull(4, Types.BOOLEAN);
|
||||
}
|
||||
|
||||
ps.setString(5, replaceNullChars(kvEntity.getJsonV()));
|
||||
|
||||
ps.setLong(6, kvEntity.getLastUpdateTs());
|
||||
ps.setObject(7, kvEntity.getEntityId());
|
||||
ps.setString(8, kvEntity.getAttrKey());
|
||||
}
|
||||
|
||||
protected void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List<Attribute> insertEntities) throws SQLException {
|
||||
Attribute kvEntity = insertEntities.get(i);
|
||||
ps.setObject(1, kvEntity.getEntityId());
|
||||
ps.setString(2, kvEntity.getAttrKey());
|
||||
|
||||
ps.setString(3, replaceNullChars(kvEntity.getStrV()));
|
||||
ps.setString(9, replaceNullChars(kvEntity.getStrV()));
|
||||
|
||||
if (kvEntity.getLongV() != null) {
|
||||
ps.setLong(4, kvEntity.getLongV());
|
||||
ps.setLong(10, kvEntity.getLongV());
|
||||
} else {
|
||||
ps.setNull(4, Types.BIGINT);
|
||||
ps.setNull(10, Types.BIGINT);
|
||||
}
|
||||
|
||||
if (kvEntity.getDblV() != null) {
|
||||
ps.setDouble(5, kvEntity.getDblV());
|
||||
ps.setDouble(11, kvEntity.getDblV());
|
||||
} else {
|
||||
ps.setNull(5, Types.DOUBLE);
|
||||
ps.setNull(11, Types.DOUBLE);
|
||||
}
|
||||
|
||||
if (kvEntity.getBoolV() != null) {
|
||||
ps.setBoolean(6, kvEntity.getBoolV());
|
||||
ps.setBoolean(12, kvEntity.getBoolV());
|
||||
} else {
|
||||
ps.setNull(6, Types.BOOLEAN);
|
||||
ps.setNull(12, Types.BOOLEAN);
|
||||
}
|
||||
|
||||
ps.setString(7, replaceNullChars(kvEntity.getJsonV()));
|
||||
ps.setString(13, replaceNullChars(kvEntity.getJsonV()));
|
||||
|
||||
ps.setLong(8, kvEntity.getLastUpdateTs());
|
||||
ps.setLong(14, kvEntity.getLastUpdateTs());
|
||||
}
|
||||
|
||||
protected String getBatchUpdateQuery() {
|
||||
return BATCH_UPDATE;
|
||||
}
|
||||
|
||||
protected String getInsertOrUpdateQuery() {
|
||||
return INSERT_OR_UPDATE;
|
||||
}
|
||||
|
||||
private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider {
|
||||
|
||||
private static final String[] COLUMNS = {"version"};
|
||||
|
||||
@Override
|
||||
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
|
||||
return con.prepareStatement(sql, COLUMNS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSql() {
|
||||
return this.sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.attribute;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
|
||||
import sanbing.jcpp.infrastructure.util.JCPPPair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AttributeRepository {
|
||||
|
||||
Optional<AttributeKvEntry> find(UUID entityId, String attrKey);
|
||||
|
||||
List<AttributeKvEntry> find(UUID entityId, Collection<String> attrKeys);
|
||||
|
||||
List<AttributeKvEntry> findAll( UUID entityId);
|
||||
|
||||
ListenableFuture<Integer> save(UUID entityId, AttributeKvEntry attribute);
|
||||
|
||||
List<ListenableFuture<String>> removeAll(UUID entityId, List<String> keys);
|
||||
|
||||
List<ListenableFuture<JCPPPair<String, Integer>>> removeAllWithVersions(UUID entityId, List<String> keys);
|
||||
|
||||
List<String> removeAllByEntityId(UUID entityId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.attribute;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import sanbing.jcpp.app.dal.entity.Attribute;
|
||||
import sanbing.jcpp.app.dal.mapper.AttributeMapper;
|
||||
import sanbing.jcpp.app.dal.repository.batch.ScheduledLogExecutorComponent;
|
||||
import sanbing.jcpp.app.dal.repository.batch.SqlBlockingQueueParams;
|
||||
import sanbing.jcpp.app.dal.repository.batch.SqlBlockingQueueWrapper;
|
||||
import sanbing.jcpp.app.dal.repository.impl.RepositoryExecutorService;
|
||||
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
|
||||
import sanbing.jcpp.infrastructure.stats.StatsFactory;
|
||||
import sanbing.jcpp.infrastructure.util.JCPPPair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class DefaultAttributeRepository implements AttributeRepository {
|
||||
|
||||
@Resource
|
||||
protected RepositoryExecutorService service;
|
||||
|
||||
@Resource
|
||||
protected JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Resource
|
||||
protected TransactionTemplate transactionTemplate;
|
||||
|
||||
@Resource
|
||||
ScheduledLogExecutorComponent logExecutor;
|
||||
|
||||
@Resource
|
||||
private AttributeMapper attributeMapper;
|
||||
|
||||
@Resource
|
||||
private AttributeKvInsertRepository attributeKvInsertRepository;
|
||||
|
||||
@Resource
|
||||
private StatsFactory statsFactory;
|
||||
|
||||
@Value("${sql.attributes.batch_size:1000}")
|
||||
private int batchSize;
|
||||
|
||||
@Value("${sql.attributes.batch_max_delay:100}")
|
||||
private long maxDelay;
|
||||
|
||||
@Value("${sql.attributes.stats_print_interval_ms:1000}")
|
||||
private long statsPrintIntervalMs;
|
||||
|
||||
@Value("${sql.attributes.batch_threads:4}")
|
||||
private int batchThreads;
|
||||
|
||||
@Value("${sql.batch_sort:true}")
|
||||
private boolean batchSortEnabled;
|
||||
|
||||
private SqlBlockingQueueWrapper<Attribute, Integer> queue;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
SqlBlockingQueueParams params = SqlBlockingQueueParams.builder()
|
||||
.logName("Attributes")
|
||||
.batchSize(batchSize)
|
||||
.maxDelay(maxDelay)
|
||||
.statsPrintIntervalMs(statsPrintIntervalMs)
|
||||
.statsNamePrefix("attributes")
|
||||
.batchSortEnabled(batchSortEnabled)
|
||||
.withResponse(true)
|
||||
.build();
|
||||
|
||||
Function<Attribute, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
|
||||
queue = new SqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory);
|
||||
queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v),
|
||||
Comparator.comparing(Attribute::getEntityId)
|
||||
.thenComparing(Attribute::getAttrKey), l -> l
|
||||
);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
private void destroy() {
|
||||
if (queue != null) {
|
||||
queue.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AttributeKvEntry> find(UUID entityId, String attrKey) {
|
||||
Attribute attributeKvEntity = attributeMapper.findByEntityAndKey(entityId, attrKey);
|
||||
if (attributeKvEntity != null) {
|
||||
return Optional.ofNullable(attributeKvEntity.toData());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AttributeKvEntry> find(UUID entityId, Collection<String> attrKeys) {
|
||||
List<Attribute> attributes = attributeMapper.findAllByIdAndAttrKey(entityId, attrKeys);
|
||||
return convertDataList(Lists.newArrayList(attributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AttributeKvEntry> findAll(UUID entityId) {
|
||||
List<Attribute> attributes = attributeMapper.findAllByEntityIdAndAttributeType(
|
||||
entityId);
|
||||
return convertDataList(Lists.newArrayList(attributes));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Integer> save(UUID entityId, AttributeKvEntry attribute) {
|
||||
Attribute entity = new Attribute();
|
||||
entity.setEntityId(entityId);
|
||||
entity.setAttrKey(attribute.getKey());
|
||||
entity.setLastUpdateTs(attribute.getLastUpdateTs());
|
||||
entity.setStrV(attribute.getStrValue().orElse(null));
|
||||
entity.setDblV(attribute.getDoubleValue().orElse(null));
|
||||
entity.setLongV(attribute.getLongValue().orElse(null));
|
||||
entity.setBoolV(attribute.getBooleanValue().orElse(null));
|
||||
entity.setJsonV(attribute.getJsonValue().orElse(null));
|
||||
return addToQueue(entity);
|
||||
}
|
||||
|
||||
private ListenableFuture<Integer> addToQueue(Attribute entity) {
|
||||
return queue.add(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ListenableFuture<String>> removeAll(UUID entityId, List<String> keys) {
|
||||
List<ListenableFuture<String>> futuresList = new ArrayList<>(keys.size());
|
||||
for (String key : keys) {
|
||||
futuresList.add(service.submit(() -> {
|
||||
attributeMapper.deleteByEntityIdAndKey(entityId, key);
|
||||
return key;
|
||||
}));
|
||||
}
|
||||
return futuresList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ListenableFuture<JCPPPair<String, Integer>>> removeAllWithVersions(UUID entityId, List<String> keys) {
|
||||
List<ListenableFuture<JCPPPair<String, Integer>>> futuresList = new ArrayList<>(keys.size());
|
||||
for (String key : keys) {
|
||||
futuresList.add(service.submit(() -> {
|
||||
Integer version = transactionTemplate.execute(status -> jdbcTemplate.query("DELETE FROM t_attr WHERE entity_id = ? " +
|
||||
"AND attr_key = ? RETURNING nextval('attr_kv_version_seq')",
|
||||
rs -> rs.next() ? rs.getInt(1) : null, entityId, key));
|
||||
return JCPPPair.of(key, version);
|
||||
}));
|
||||
}
|
||||
return futuresList;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public List<String> removeAllByEntityId(UUID entityId) {
|
||||
return jdbcTemplate.queryForList("DELETE FROM t_attr WHERE entity_id = ? " +
|
||||
"RETURNING attr_key", entityId).stream()
|
||||
.map(row -> row.get("attr_key").toString())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<AttributeKvEntry> convertDataList(Collection<Attribute> toConvert) {
|
||||
if (CollectionUtils.isEmpty(toConvert)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<AttributeKvEntry> converted = new ArrayList<>(toConvert.size());
|
||||
for (Attribute attribute : toConvert) {
|
||||
if (attribute != null) {
|
||||
converted.add(attribute.toData());
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.attribute;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
|
||||
import sanbing.jcpp.app.data.kv.KvEntry;
|
||||
import sanbing.jcpp.infrastructure.util.exception.DataValidationException;
|
||||
import sanbing.jcpp.infrastructure.util.exception.IncorrectParameterException;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXssValidator;
|
||||
import sanbing.jcpp.infrastructure.util.validation.Validator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class KvValidator {
|
||||
|
||||
private static final Cache<String, Boolean> validatedKeys;
|
||||
|
||||
static {
|
||||
validatedKeys = Caffeine.newBuilder()
|
||||
.expireAfterAccess(24, TimeUnit.HOURS)
|
||||
.maximumSize(50000).build();
|
||||
}
|
||||
|
||||
public static void validate(List<? extends KvEntry> tsKvEntries, boolean valueNoXssValidation) {
|
||||
tsKvEntries.forEach(tsKvEntry -> validate(tsKvEntry, valueNoXssValidation));
|
||||
}
|
||||
|
||||
public static void validate(KvEntry tsKvEntry, boolean valueNoXssValidation) {
|
||||
if (tsKvEntry == null) {
|
||||
throw new IncorrectParameterException("键值条目不能为空");
|
||||
}
|
||||
|
||||
String key = tsKvEntry.getKey();
|
||||
|
||||
if (StringUtils.isBlank(key)) {
|
||||
throw new DataValidationException("键不能为空");
|
||||
}
|
||||
|
||||
if (key.length() > 255) {
|
||||
throw new DataValidationException("验证错误:键的长度必须小于或等于255");
|
||||
}
|
||||
|
||||
Boolean isValid = validatedKeys.asMap().get(key);
|
||||
if (isValid == null) {
|
||||
isValid = NoXssValidator.isValid(key);
|
||||
validatedKeys.put(key, isValid);
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new DataValidationException("验证错误:键的格式不正确");
|
||||
}
|
||||
|
||||
if (valueNoXssValidation) {
|
||||
Object value = tsKvEntry.getValue();
|
||||
if (value instanceof CharSequence || value instanceof JsonNode) {
|
||||
if (!NoXssValidator.isValid(value.toString())) {
|
||||
throw new DataValidationException("验证错误:值的格式不正确");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void validateId(UUID id) {
|
||||
Validator.validateId(id, uuid -> "ID不正确: " + uuid);
|
||||
}
|
||||
|
||||
public static void validateAttributeList(List<AttributeKvEntry> kvEntries, boolean valueNoXssValidation) {
|
||||
kvEntries.forEach(tsKvEntry -> validateAttribute(tsKvEntry, valueNoXssValidation));
|
||||
}
|
||||
|
||||
public static void validateAttribute(AttributeKvEntry kvEntry, boolean valueNoXssValidation) {
|
||||
validate(kvEntry, valueNoXssValidation);
|
||||
if (kvEntry.getDataType() == null) {
|
||||
throw new IncorrectParameterException("键值条目的数据类型不能为空");
|
||||
} else {
|
||||
Validator.validateString(kvEntry.getKey(), "键值条目错误:键不能为空");
|
||||
Validator.validatePositiveNumber(kvEntry.getLastUpdateTs(), "最后更新时间戳错误:时间戳必须为正数");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class ScheduledLogExecutorComponent {
|
||||
|
||||
private ScheduledExecutorService schedulerLogExecutor;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
schedulerLogExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||
JCPPThreadFactory.forName("sql-log-%d")
|
||||
);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (schedulerLogExecutor != null) {
|
||||
schedulerLogExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
|
||||
schedulerLogExecutor.scheduleAtFixedRate(command, initialDelay, period, unit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sanbing.jcpp.infrastructure.stats.MessagesStats;
|
||||
import sanbing.jcpp.infrastructure.util.CollectionsUtil;
|
||||
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class SqlBlockingQueue<E, R> implements SqlQueue<E, R> {
|
||||
|
||||
private final BlockingQueue<SqlQueueElement<E, R>> queue = new LinkedBlockingQueue<>();
|
||||
private final SqlBlockingQueueParams params;
|
||||
|
||||
private ExecutorService executor;
|
||||
private final MessagesStats stats;
|
||||
|
||||
public SqlBlockingQueue(SqlBlockingQueueParams params, MessagesStats stats) {
|
||||
this.params = params;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<SqlQueueElement<E, R>>, List<SqlQueueElement<E, R>>> filter, int index) {
|
||||
executor = Executors.newSingleThreadExecutor(JCPPThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase()));
|
||||
executor.submit(() -> {
|
||||
String logName = params.getLogName();
|
||||
int batchSize = params.getBatchSize();
|
||||
long maxDelay = params.getMaxDelay();
|
||||
final List<SqlQueueElement<E, R>> entities = new ArrayList<>(batchSize);
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
long currentTs = System.currentTimeMillis();
|
||||
SqlQueueElement<E, R> attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS);
|
||||
if (attr == null) {
|
||||
continue;
|
||||
} else {
|
||||
entities.add(attr);
|
||||
}
|
||||
queue.drainTo(entities, batchSize - 1);
|
||||
boolean fullPack = entities.size() == batchSize;
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[{}] Going to save {} entities", logName, entities.size());
|
||||
log.trace("[{}] Going to save entities: {}", logName, entities);
|
||||
}
|
||||
|
||||
List<SqlQueueElement<E, R>> entitiesToSave = filter.apply(entities);
|
||||
|
||||
if (params.isBatchSortEnabled()) {
|
||||
entitiesToSave = entitiesToSave.stream().sorted((o1, o2) -> batchUpdateComparator.compare(o1.entity(), o2.entity())).toList();
|
||||
}
|
||||
|
||||
List<R> result = saveFunction.apply(entitiesToSave.stream().map(SqlQueueElement::entity).collect(Collectors.toList()));
|
||||
|
||||
if (params.isWithResponse()) {
|
||||
for (int i = 0; i < entitiesToSave.size(); i++) {
|
||||
entitiesToSave.get(i).future().set(result.get(i));
|
||||
}
|
||||
|
||||
if (entities.size() > entitiesToSave.size()) {
|
||||
CollectionsUtil.diffLists(entitiesToSave, entities).forEach(v -> v.future().set(null));
|
||||
}
|
||||
} else {
|
||||
entities.forEach(v -> v.future().set(null));
|
||||
}
|
||||
|
||||
stats.incrementSuccessful(entities.size());
|
||||
if (!fullPack) {
|
||||
long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs);
|
||||
if (remainingDelay > 0) {
|
||||
Thread.sleep(remainingDelay);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
if (t instanceof InterruptedException) {
|
||||
log.info("[{}] Queue polling was interrupted", logName);
|
||||
break;
|
||||
} else {
|
||||
log.error("[{}] Failed to save {} entities", logName, entities.size(), t);
|
||||
try {
|
||||
stats.incrementFailed(entities.size());
|
||||
entities.forEach(entityFutureWrapper -> entityFutureWrapper.future().setException(t));
|
||||
} catch (Throwable th) {
|
||||
log.error("[{}] Failed to set future exception", logName, th);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
entities.clear();
|
||||
}
|
||||
}
|
||||
log.info("[{}] Queue polling completed", logName);
|
||||
});
|
||||
|
||||
logExecutor.scheduleAtFixedRate(() -> {
|
||||
if (!queue.isEmpty() || stats.getTotal() > 0 || stats.getSuccessful() > 0 || stats.getFailed() > 0) {
|
||||
log.info("Queue-{} [{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]", index,
|
||||
params.getLogName(), queue.size(), stats.getTotal(), stats.getSuccessful(), stats.getFailed());
|
||||
stats.reset();
|
||||
}
|
||||
}, params.getStatsPrintIntervalMs(), params.getStatsPrintIntervalMs(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<R> add(E element) {
|
||||
SettableFuture<R> future = SettableFuture.create();
|
||||
queue.add(new SqlQueueElement<>(future, element));
|
||||
stats.incrementTotal();
|
||||
return future;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@Builder
|
||||
public class SqlBlockingQueueParams {
|
||||
|
||||
private final String logName;
|
||||
private final int batchSize;
|
||||
private final long maxDelay;
|
||||
private final long statsPrintIntervalMs;
|
||||
private final String statsNamePrefix;
|
||||
private final boolean batchSortEnabled;
|
||||
private final boolean withResponse;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sanbing.jcpp.infrastructure.stats.MessagesStats;
|
||||
import sanbing.jcpp.infrastructure.stats.StatsFactory;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
public class SqlBlockingQueueWrapper<E, R> {
|
||||
private final CopyOnWriteArrayList<SqlBlockingQueue<E, R>> queues = new CopyOnWriteArrayList<>();
|
||||
private final SqlBlockingQueueParams params;
|
||||
private final Function<E, Integer> hashCodeFunction;
|
||||
private final int maxThreads;
|
||||
private final StatsFactory statsFactory;
|
||||
|
||||
/**
|
||||
* Starts JCPPSqlBlockingQueues.
|
||||
*
|
||||
* @param logExecutor executor that will be printing logs and statistics
|
||||
* @param saveFunction function to save entities in database
|
||||
* @param batchUpdateComparator comparator to sort entities by primary key to avoid deadlocks in cluster mode
|
||||
* NOTE: you must use all of primary key parts in your comparator
|
||||
*/
|
||||
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator) {
|
||||
init(logExecutor, l -> { saveFunction.accept(l); return null; }, batchUpdateComparator, l -> l);
|
||||
}
|
||||
|
||||
public void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<SqlQueueElement<E, R>>, List<SqlQueueElement<E, R>>> filter) {
|
||||
for (int i = 0; i < maxThreads; i++) {
|
||||
MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i);
|
||||
SqlBlockingQueue<E, R> queue = new SqlBlockingQueue<>(params, stats);
|
||||
queues.add(queue);
|
||||
queue.init(logExecutor, saveFunction, batchUpdateComparator, filter, i);
|
||||
}
|
||||
}
|
||||
|
||||
public ListenableFuture<R> add(E element) {
|
||||
int queueIndex = element != null ? (hashCodeFunction.apply(element) & 0x7FFFFFFF) % maxThreads : 0;
|
||||
return queues.get(queueIndex).add(element);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
queues.forEach(SqlBlockingQueue::destroy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface SqlQueue<E, R> {
|
||||
|
||||
void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<SqlQueueElement<E, R>>, List<SqlQueueElement<E, R>>> filter, int queueIndex);
|
||||
|
||||
void destroy();
|
||||
|
||||
ListenableFuture<R> add(E element);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.batch;
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
public record SqlQueueElement<E, R>(SettableFuture<R> future, E entity) {
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.repository;
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import sanbing.jcpp.infrastructure.cache.TransactionalCache;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public abstract class AbstractCachedEntityRepository<K extends Serializable, V extends Serializable, E> extends AbstractEntityRepository {
|
||||
|
||||
@Autowired
|
||||
protected TransactionalCache<K, V> cache;
|
||||
|
||||
protected void publishEvictEvent(E event) {
|
||||
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
eventPublisher.publishEvent(event);
|
||||
@@ -4,7 +4,7 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.repository;
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -4,9 +4,9 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.repository;
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
import sanbing.jcpp.infrastructure.cache.VersionedCache;
|
||||
import sanbing.jcpp.infrastructure.cache.VersionedCacheKey;
|
||||
@@ -15,7 +15,7 @@ import java.io.Serializable;
|
||||
|
||||
public abstract class CachedVersionedEntityRepository<K extends VersionedCacheKey, V extends Serializable & HasVersion, E> extends AbstractCachedEntityRepository<K, V, E> {
|
||||
|
||||
@Resource
|
||||
@Autowired
|
||||
protected VersionedCache<K, V> cache;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import sanbing.jcpp.app.dal.entity.Gun;
|
||||
import sanbing.jcpp.app.dal.mapper.GunMapper;
|
||||
import sanbing.jcpp.app.dal.repository.GunRepository;
|
||||
import sanbing.jcpp.app.service.cache.gun.GunCacheEvictEvent;
|
||||
import sanbing.jcpp.app.service.cache.gun.GunCacheKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static sanbing.jcpp.infrastructure.util.validation.Validator.validateId;
|
||||
import static sanbing.jcpp.infrastructure.util.validation.Validator.validateString;
|
||||
|
||||
/**
|
||||
* 充电枪数据访问实现
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Repository
|
||||
@Slf4j
|
||||
public class GunRepositoryImpl extends CachedVersionedEntityRepository<GunCacheKey, Gun, GunCacheEvictEvent> implements GunRepository {
|
||||
|
||||
@Resource
|
||||
GunMapper gunMapper;
|
||||
|
||||
@TransactionalEventListener(classes = GunCacheEvictEvent.class)
|
||||
@Override
|
||||
public void handleEvictEvent(GunCacheEvictEvent event) {
|
||||
// 如果修改或删除充电枪,需要在这里消费删除事件
|
||||
List<GunCacheKey> toEvict = new ArrayList<>(3);
|
||||
|
||||
// 基于gunId的缓存key
|
||||
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()));
|
||||
}
|
||||
|
||||
cache.evict(toEvict);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Gun findByPileCodeAndGunCode(String pileCode, String gunCode) {
|
||||
validateString(pileCode, code -> "无效的桩编号: " + pileCode);
|
||||
validateString(gunCode, code -> "无效的枪编号: " + gunCode);
|
||||
|
||||
return cache.get(new GunCacheKey(pileCode, gunCode),
|
||||
() -> gunMapper.selectByPileCodeAndGunCode(pileCode, gunCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Gun findById(UUID gunId) {
|
||||
validateId(gunId, id -> "无效的充电枪ID: " + gunId);
|
||||
|
||||
return cache.get(new GunCacheKey(gunId),
|
||||
() -> gunMapper.selectById(gunId));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.repository;
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import sanbing.jcpp.app.dal.entity.Pile;
|
||||
import sanbing.jcpp.app.dal.mapper.PileMapper;
|
||||
import sanbing.jcpp.app.dal.repository.PileRepository;
|
||||
import sanbing.jcpp.app.service.cache.pile.PileCacheEvictEvent;
|
||||
import sanbing.jcpp.app.service.cache.pile.PileCacheKey;
|
||||
|
||||
@@ -21,7 +22,7 @@ import java.util.List;
|
||||
import static sanbing.jcpp.infrastructure.util.validation.Validator.validateString;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
@Repository
|
||||
@Slf4j
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.dal.repository.impl;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import sanbing.jcpp.infrastructure.util.async.AbstractListeningExecutor;
|
||||
|
||||
@Component
|
||||
public class RepositoryExecutorService extends AbstractListeningExecutor {
|
||||
|
||||
@Value("${spring.datasource.hikari.maximum-pool-size}")
|
||||
private int poolSize;
|
||||
|
||||
@Override
|
||||
protected int getThreadPollSize() {
|
||||
return poolSize;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据库安装模式枚举
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Getter
|
||||
public enum InstallModeEnum {
|
||||
|
||||
/**
|
||||
* 初始化数据库,执行schema-init.sql并加载演示数据
|
||||
*/
|
||||
INIT("init", "初始化数据库"),
|
||||
|
||||
/**
|
||||
* 升级数据库,根据版本执行升级脚本
|
||||
*/
|
||||
UPGRADE("upgrade", "升级数据库"),
|
||||
|
||||
/**
|
||||
* 不执行任何操作
|
||||
*/
|
||||
DISABLED("disabled", "禁用安装功能");
|
||||
|
||||
private final String mode;
|
||||
private final String description;
|
||||
|
||||
InstallModeEnum(String mode, String description) {
|
||||
this.mode = mode;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据mode字符串获取枚举值
|
||||
*/
|
||||
public static InstallModeEnum fromMode(String mode) {
|
||||
if (mode == null || mode.isEmpty()) {
|
||||
return DISABLED;
|
||||
}
|
||||
|
||||
for (InstallModeEnum installMode : values()) {
|
||||
if (installMode.mode.equals(mode)) {
|
||||
return installMode;
|
||||
}
|
||||
}
|
||||
|
||||
return DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
@Data
|
||||
public class PileSession implements Serializable {
|
||||
@@ -37,6 +37,8 @@ public class PileSession implements Serializable {
|
||||
|
||||
private int nodeGrpcPort;
|
||||
|
||||
|
||||
|
||||
public PileSession(UUID pileId, String pileCode, String protocolName) {
|
||||
this.pileId = pileId;
|
||||
this.pileCode = pileCode;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* 属性键枚举,定义系统内置的属性键
|
||||
* 使用String类型提高可读性
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public enum AttrKeyEnum {
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
STATUS( "status"),
|
||||
|
||||
/**
|
||||
* 连接时间
|
||||
*/
|
||||
CONNECTED_AT("connectedAt"),
|
||||
|
||||
/**
|
||||
* 断开连接时间
|
||||
*/
|
||||
DISCONNECTED_AT("disconnectedAt"),
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
LAST_ACTIVE_TIME("lastActiveTime"),
|
||||
|
||||
/**
|
||||
* 充电枪运行状态
|
||||
*/
|
||||
GUN_RUN_STATUS("gunRunStatus"),
|
||||
|
||||
/**
|
||||
* 地锁状态
|
||||
*/
|
||||
LOCK_STATUS("lockStatus"),
|
||||
|
||||
/**
|
||||
* 车位状态
|
||||
*/
|
||||
PARK_STATUS("parkStatus");
|
||||
|
||||
@JsonValue
|
||||
private final String code;
|
||||
|
||||
AttrKeyEnum( String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
|
||||
import sanbing.jcpp.infrastructure.cache.HasVersion;
|
||||
|
||||
|
||||
public interface AttributeKvEntry extends KvEntry, HasVersion {
|
||||
|
||||
long getLastUpdateTs();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public record AttributesSaveResult(List<Integer> versions) {
|
||||
|
||||
public static final AttributesSaveResult EMPTY = new AttributesSaveResult(Collections.emptyList());
|
||||
|
||||
public static AttributesSaveResult of(List<Integer> versions) {
|
||||
if (versions == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
return new AttributesSaveResult(versions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
public class BaseAttributeKvEntry implements AttributeKvEntry {
|
||||
|
||||
private static final long serialVersionUID = -6460767583563159407L;
|
||||
|
||||
private final long lastUpdateTs;
|
||||
@Valid
|
||||
private final KvEntry kv;
|
||||
|
||||
private final Integer version;
|
||||
|
||||
public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) {
|
||||
this.kv = kv;
|
||||
this.lastUpdateTs = lastUpdateTs;
|
||||
this.version = null;
|
||||
}
|
||||
|
||||
public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs, Integer version) {
|
||||
this.kv = kv;
|
||||
this.lastUpdateTs = lastUpdateTs;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) {
|
||||
this(kv, lastUpdateTs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return kv.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return kv.getDataType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getStrValue() {
|
||||
return kv.getStrValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> getLongValue() {
|
||||
return kv.getLongValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> getBooleanValue() {
|
||||
return kv.getBooleanValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Double> getDoubleValue() {
|
||||
return kv.getDoubleValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getJsonValue() {
|
||||
return kv.getJsonValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return kv.getValueAsString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return kv.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前对象转换为JSON字节数组
|
||||
* 避免Jackson序列化Optional类型的问题
|
||||
*/
|
||||
@JsonIgnore
|
||||
public byte[] toJsonBytes() {
|
||||
try {
|
||||
ObjectNode json = JacksonUtil.newObjectNode();
|
||||
json.put("lastUpdateTs", lastUpdateTs);
|
||||
if (version != null) {
|
||||
json.put("version", version);
|
||||
}
|
||||
|
||||
// 处理KvEntry序列化
|
||||
ObjectNode kvJson = JacksonUtil.newObjectNode();
|
||||
kvJson.put("key", kv.getKey());
|
||||
kvJson.put("dataType", kv.getDataType().name());
|
||||
|
||||
// 根据数据类型序列化值,避免Optional问题
|
||||
switch (kv.getDataType()) {
|
||||
case STRING:
|
||||
kv.getStrValue().ifPresent(value -> kvJson.put("value", value));
|
||||
break;
|
||||
case LONG:
|
||||
kv.getLongValue().ifPresent(value -> kvJson.put("value", value));
|
||||
break;
|
||||
case BOOLEAN:
|
||||
kv.getBooleanValue().ifPresent(value -> kvJson.put("value", value));
|
||||
break;
|
||||
case DOUBLE:
|
||||
kv.getDoubleValue().ifPresent(value -> kvJson.put("value", value));
|
||||
break;
|
||||
case JSON:
|
||||
kv.getJsonValue().ifPresent(value -> kvJson.put("value", value));
|
||||
break;
|
||||
default:
|
||||
// 如果没有匹配的类型,将值作为字符串处理
|
||||
kvJson.put("value", kv.getValueAsString());
|
||||
break;
|
||||
}
|
||||
|
||||
json.set("kv", kvJson);
|
||||
return JacksonUtil.writeValueAsBytes(json);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to serialize BaseAttributeKvEntry to JSON bytes", e);
|
||||
throw new RuntimeException("Failed to serialize BaseAttributeKvEntry", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON字节数组反序列化为BaseAttributeKvEntry对象
|
||||
* 避免Jackson反序列化Optional类型的问题
|
||||
*/
|
||||
public static BaseAttributeKvEntry fromJsonBytes(byte[] jsonBytes) {
|
||||
try {
|
||||
JsonNode json = JacksonUtil.fromBytes(jsonBytes);
|
||||
|
||||
long lastUpdateTs = json.get("lastUpdateTs").asLong();
|
||||
Integer version = json.has("version") ? json.get("version").asInt() : null;
|
||||
|
||||
// 解析KvEntry
|
||||
JsonNode kvJson = json.get("kv");
|
||||
String key = kvJson.get("key").asText();
|
||||
DataType dataType = DataType.valueOf(kvJson.get("dataType").asText());
|
||||
|
||||
KvEntry kvEntry;
|
||||
switch (dataType) {
|
||||
case STRING:
|
||||
String strValue = kvJson.has("value") ? kvJson.get("value").asText() : null;
|
||||
kvEntry = new StringDataEntry(key, strValue);
|
||||
break;
|
||||
case LONG:
|
||||
Long longValue = kvJson.has("value") ? kvJson.get("value").asLong() : null;
|
||||
kvEntry = new LongDataEntry(key, longValue);
|
||||
break;
|
||||
case BOOLEAN:
|
||||
Boolean boolValue = kvJson.has("value") ? kvJson.get("value").asBoolean() : null;
|
||||
kvEntry = new BooleanDataEntry(key, boolValue);
|
||||
break;
|
||||
case DOUBLE:
|
||||
Double doubleValue = kvJson.has("value") ? kvJson.get("value").asDouble() : null;
|
||||
kvEntry = new DoubleDataEntry(key, doubleValue);
|
||||
break;
|
||||
case JSON:
|
||||
String jsonValue = kvJson.has("value") ? kvJson.get("value").asText() : null;
|
||||
kvEntry = new JsonDataEntry(key, jsonValue);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported data type: " + dataType);
|
||||
}
|
||||
|
||||
return new BaseAttributeKvEntry(kvEntry, lastUpdateTs, version);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to deserialize BaseAttributeKvEntry from JSON bytes", e);
|
||||
throw new RuntimeException("Failed to deserialize BaseAttributeKvEntry", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
|
||||
import sanbing.jcpp.infrastructure.util.validation.Length;
|
||||
import sanbing.jcpp.infrastructure.util.validation.NoXss;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class BasicKvEntry implements KvEntry {
|
||||
|
||||
@Length(fieldName = "attribute key")
|
||||
@NoXss
|
||||
private final String key;
|
||||
|
||||
protected BasicKvEntry(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getStrValue() {
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> getLongValue() {
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> getBooleanValue() {
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Double> getDoubleValue() {
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getJsonValue() {
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof BasicKvEntry that)) return false;
|
||||
return Objects.equals(key, that.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BasicKvEntry{" +
|
||||
"key='" + key + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BooleanDataEntry extends BasicKvEntry {
|
||||
private final Boolean value;
|
||||
|
||||
public BooleanDataEntry(String key, Boolean value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return DataType.BOOLEAN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> getBooleanValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof BooleanDataEntry that)) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BooleanDataEntry{" +
|
||||
"value=" + value +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Boolean.toString(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum DataType {
|
||||
|
||||
BOOLEAN(0),
|
||||
LONG(1),
|
||||
DOUBLE(2),
|
||||
STRING(3),
|
||||
JSON(4);
|
||||
|
||||
@Getter
|
||||
private final int protoNumber; // Corresponds to KeyValueType
|
||||
|
||||
DataType(int protoNumber) {
|
||||
this.protoNumber = protoNumber;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class DoubleDataEntry extends BasicKvEntry {
|
||||
|
||||
private final Double value;
|
||||
|
||||
public DoubleDataEntry(String key, Double value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return DataType.DOUBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Double> getDoubleValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DoubleDataEntry that)) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DoubleDataEntry{" +
|
||||
"value=" + value +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Double.toString(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class JsonDataEntry extends BasicKvEntry {
|
||||
|
||||
private final String value;
|
||||
|
||||
public JsonDataEntry(String key, String value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return DataType.JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getJsonValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof JsonDataEntry that)) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JsonDataEntry{" +
|
||||
"value=" + value +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
31
jcpp-app/src/main/java/sanbing/jcpp/app/data/kv/KvEntry.java
Normal file
31
jcpp-app/src/main/java/sanbing/jcpp/app/data/kv/KvEntry.java
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface KvEntry extends Serializable {
|
||||
|
||||
String getKey();
|
||||
|
||||
DataType getDataType();
|
||||
|
||||
Optional<String> getStrValue();
|
||||
|
||||
Optional<Long> getLongValue();
|
||||
|
||||
Optional<Boolean> getBooleanValue();
|
||||
|
||||
Optional<Double> getDoubleValue();
|
||||
|
||||
Optional<String> getJsonValue();
|
||||
|
||||
String getValueAsString();
|
||||
|
||||
Object getValue();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class LongDataEntry extends BasicKvEntry {
|
||||
|
||||
private final Long value;
|
||||
|
||||
public LongDataEntry(String key, Long value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return DataType.LONG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> getLongValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof LongDataEntry that)) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LongDataEntry{" +
|
||||
"value=" + value +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Long.toString(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.kv;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class StringDataEntry extends BasicKvEntry {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String value;
|
||||
|
||||
public StringDataEntry(String key, String value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
return DataType.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getStrValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof StringDataEntry that))
|
||||
return false;
|
||||
if (!super.equals(o))
|
||||
return false;
|
||||
return Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.data.page;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* 分页数据迭代器,用于处理大数据量查询,避免内存溢出
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
* @author 九筒
|
||||
*/
|
||||
public class PageDataIterable<T> implements Iterable<T> {
|
||||
|
||||
private final FetchFunction<T> fetchFunction;
|
||||
private final int pageSize;
|
||||
|
||||
public PageDataIterable(FetchFunction<T> fetchFunction, int pageSize) {
|
||||
this.fetchFunction = fetchFunction;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return new PageDataIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取函数接口
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FetchFunction<T> {
|
||||
/**
|
||||
* 获取指定页的数据
|
||||
*
|
||||
* @param offset 偏移量
|
||||
* @param limit 限制数量
|
||||
* @return 数据列表
|
||||
*/
|
||||
List<T> fetch(int offset, int limit);
|
||||
}
|
||||
|
||||
private class PageDataIterator implements Iterator<T> {
|
||||
private int currentOffset = 0;
|
||||
private List<T> currentPage;
|
||||
private int currentIndex = 0;
|
||||
private boolean hasMorePages = true;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
// 如果当前页还有数据,直接返回true
|
||||
if (currentPage != null && currentIndex < currentPage.size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果没有更多页了,返回false
|
||||
if (!hasMorePages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试加载下一页
|
||||
loadNextPage();
|
||||
|
||||
// 检查加载后是否有数据
|
||||
return currentPage != null && !currentPage.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return currentPage.get(currentIndex++);
|
||||
}
|
||||
|
||||
private void loadNextPage() {
|
||||
try {
|
||||
currentPage = fetchFunction.fetch(currentOffset, pageSize);
|
||||
currentIndex = 0;
|
||||
|
||||
// 如果返回的数据少于页大小,说明没有更多页了
|
||||
if (currentPage == null || currentPage.size() < pageSize) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
// 更新偏移量
|
||||
currentOffset += pageSize;
|
||||
|
||||
} catch (Exception e) {
|
||||
hasMorePages = false;
|
||||
currentPage = null;
|
||||
throw new RuntimeException("Failed to fetch next page", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class JCPPCredentialsExpiredResponse extends JCPPErrorResponse {
|
||||
|
||||
@Getter
|
||||
private final String resetToken;
|
||||
|
||||
protected JCPPCredentialsExpiredResponse(String message, String resetToken) {
|
||||
super(message, JCPPErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
this.resetToken = resetToken;
|
||||
}
|
||||
|
||||
public static JCPPCredentialsExpiredResponse of(final String message, final String resetToken) {
|
||||
return new JCPPCredentialsExpiredResponse(message, resetToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class JCPPCredentialsViolationResponse extends JCPPErrorResponse {
|
||||
|
||||
protected JCPPCredentialsViolationResponse(String message) {
|
||||
super(message, JCPPErrorCode.PASSWORD_VIOLATION, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public static JCPPCredentialsViolationResponse of(final String message) {
|
||||
return new JCPPCredentialsViolationResponse(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum JCPPErrorCode {
|
||||
|
||||
GENERAL(2),
|
||||
AUTHENTICATION(10),
|
||||
JWT_TOKEN_EXPIRED(11),
|
||||
CREDENTIALS_EXPIRED(15),
|
||||
PERMISSION_DENIED(20),
|
||||
INVALID_ARGUMENTS(30),
|
||||
BAD_REQUEST_PARAMS(31),
|
||||
ITEM_NOT_FOUND(32),
|
||||
TOO_MANY_REQUESTS(33),
|
||||
TOO_MANY_UPDATES(34),
|
||||
VERSION_CONFLICT(35),
|
||||
SUBSCRIPTION_VIOLATION(40),
|
||||
PASSWORD_VIOLATION(45),
|
||||
DATABASE(46);
|
||||
|
||||
private final int errorCode;
|
||||
|
||||
JCPPErrorCode(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Data
|
||||
public class JCPPErrorResponse {
|
||||
private final HttpStatus status;
|
||||
|
||||
private final String message;
|
||||
|
||||
private final JCPPErrorCode errorCode;
|
||||
|
||||
private final long timestamp;
|
||||
|
||||
protected JCPPErrorResponse(final String message, final JCPPErrorCode errorCode, HttpStatus status) {
|
||||
this.message = message;
|
||||
this.errorCode = errorCode;
|
||||
this.status = status;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static JCPPErrorResponse of(final String message, final JCPPErrorCode errorCode, HttpStatus status) {
|
||||
return new JCPPErrorResponse(message, errorCode, status);
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status.value();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
import sanbing.jcpp.app.service.security.exception.AuthMethodNotSupportedException;
|
||||
import sanbing.jcpp.app.service.security.exception.JwtExpiredTokenException;
|
||||
import sanbing.jcpp.app.service.security.exception.UserPasswordExpiredException;
|
||||
import sanbing.jcpp.app.service.security.exception.UserPasswordNotValidException;
|
||||
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RestControllerAdvice
|
||||
public class JCPPErrorResponseHandler extends ResponseEntityExceptionHandler implements AccessDeniedHandler, ErrorController {
|
||||
|
||||
private static final Map<HttpStatus, JCPPErrorCode> statusToErrorCodeMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
statusToErrorCodeMap.put(HttpStatus.BAD_REQUEST, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
statusToErrorCodeMap.put(HttpStatus.UNAUTHORIZED, JCPPErrorCode.AUTHENTICATION);
|
||||
statusToErrorCodeMap.put(HttpStatus.FORBIDDEN, JCPPErrorCode.PERMISSION_DENIED);
|
||||
statusToErrorCodeMap.put(HttpStatus.NOT_FOUND, JCPPErrorCode.ITEM_NOT_FOUND);
|
||||
statusToErrorCodeMap.put(HttpStatus.METHOD_NOT_ALLOWED, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
statusToErrorCodeMap.put(HttpStatus.NOT_ACCEPTABLE, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
statusToErrorCodeMap.put(HttpStatus.UNSUPPORTED_MEDIA_TYPE, JCPPErrorCode.BAD_REQUEST_PARAMS);
|
||||
statusToErrorCodeMap.put(HttpStatus.TOO_MANY_REQUESTS, JCPPErrorCode.TOO_MANY_REQUESTS);
|
||||
statusToErrorCodeMap.put(HttpStatus.INTERNAL_SERVER_ERROR, JCPPErrorCode.GENERAL);
|
||||
statusToErrorCodeMap.put(HttpStatus.SERVICE_UNAVAILABLE, JCPPErrorCode.GENERAL);
|
||||
}
|
||||
|
||||
private static final Map<JCPPErrorCode, HttpStatus> errorCodeToStatusMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.GENERAL, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.INVALID_ARGUMENTS, HttpStatus.BAD_REQUEST);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN);
|
||||
errorCodeToStatusMap.put(JCPPErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
private static JCPPErrorCode statusToErrorCode(HttpStatus status) {
|
||||
return statusToErrorCodeMap.getOrDefault(status, JCPPErrorCode.GENERAL);
|
||||
}
|
||||
|
||||
private static HttpStatus errorCodeToStatus(JCPPErrorCode errorCode) {
|
||||
return errorCodeToStatusMap.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@RequestMapping("/error")
|
||||
public ResponseEntity<Object> handleError(HttpServletRequest request) {
|
||||
HttpStatus httpStatus = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE))
|
||||
.map(status -> HttpStatus.resolve(Integer.parseInt(status.toString())))
|
||||
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
String errorMessage = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION))
|
||||
.map(e -> (ExceptionUtils.getMessage((Throwable) e)))
|
||||
.orElse(httpStatus.getReasonPhrase());
|
||||
return new ResponseEntity<>(JCPPErrorResponse.of(errorMessage, statusToErrorCode(httpStatus), httpStatus), httpStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) throws IOException,
|
||||
ServletException {
|
||||
if (!response.isCommitted()) {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
JacksonUtil.writeValue(response.getWriter(),
|
||||
JCPPErrorResponse.of("You don't have permission to perform this operation!",
|
||||
JCPPErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public void handle(Exception exception, HttpServletResponse response) {
|
||||
log.debug("Processing exception {}", exception.getMessage(), exception);
|
||||
if (!response.isCommitted()) {
|
||||
try {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
switch (exception) {
|
||||
case JCPPException jcppException -> {
|
||||
if (jcppException.getErrorCode() == JCPPErrorCode.SUBSCRIPTION_VIOLATION) {
|
||||
handleSubscriptionException(jcppException, response);
|
||||
} else if (jcppException.getErrorCode() == JCPPErrorCode.DATABASE) {
|
||||
handleDatabaseException(jcppException.getCause(), response);
|
||||
} else {
|
||||
handleJCPPException(jcppException, response);
|
||||
}
|
||||
}
|
||||
case AccessDeniedException ignored -> handleAccessDeniedException(response);
|
||||
case AuthenticationException authenticationException ->
|
||||
handleAuthenticationException(authenticationException, response);
|
||||
default -> {
|
||||
if (exception instanceof DataAccessException e) {
|
||||
handleDatabaseException(e, response);
|
||||
} else {
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of(exception.getMessage(),
|
||||
JCPPErrorCode.GENERAL, HttpStatus.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Can't handle exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleExceptionInternal(
|
||||
Exception ex, @Nullable Object body,
|
||||
HttpHeaders headers, HttpStatusCode statusCode,
|
||||
WebRequest request) {
|
||||
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(statusCode)) {
|
||||
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
|
||||
}
|
||||
JCPPErrorCode errorCode = statusToErrorCode((HttpStatus) statusCode);
|
||||
return new ResponseEntity<>(JCPPErrorResponse.of(ex.getMessage(), errorCode, (HttpStatus) statusCode), headers, statusCode);
|
||||
}
|
||||
|
||||
private void handleJCPPException(JCPPException jcppException, HttpServletResponse response) throws IOException {
|
||||
JCPPErrorCode errorCode = jcppException.getErrorCode();
|
||||
HttpStatus status = errorCodeToStatus(errorCode);
|
||||
response.setStatus(status.value());
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of(jcppException.getMessage(), errorCode, status));
|
||||
}
|
||||
|
||||
private void handleSubscriptionException(JCPPException subscriptionException, HttpServletResponse response) throws IOException {
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
JacksonUtil.writeValue(response.getWriter(),
|
||||
JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class));
|
||||
}
|
||||
|
||||
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
|
||||
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
|
||||
JCPPErrorResponse errorResponse = JCPPErrorResponse.of("Database error", JCPPErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
writeResponse(errorResponse, response);
|
||||
}
|
||||
|
||||
private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
JacksonUtil.writeValue(response.getWriter(),
|
||||
JCPPErrorResponse.of("You don't have permission to perform this operation!",
|
||||
JCPPErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN));
|
||||
|
||||
}
|
||||
|
||||
private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of("Invalid username or password", JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
} else if (authenticationException instanceof DisabledException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of("User account is not active", JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
} else if (authenticationException instanceof LockedException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of("User account is locked due to security policy", JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
} else if (authenticationException instanceof JwtExpiredTokenException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of("Token has expired", JCPPErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
|
||||
} else if (authenticationException instanceof AuthMethodNotSupportedException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of(authenticationException.getMessage(), JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
} else if (authenticationException instanceof UserPasswordExpiredException expiredException) {
|
||||
String resetToken = expiredException.getResetToken();
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken));
|
||||
} else if (authenticationException instanceof UserPasswordNotValidException expiredException) {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPCredentialsViolationResponse.of(expiredException.getMessage()));
|
||||
} else {
|
||||
JacksonUtil.writeValue(response.getWriter(), JCPPErrorResponse.of("Authentication failed", JCPPErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeResponse(JCPPErrorResponse errorResponse, HttpServletResponse response) throws IOException {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setStatus(errorResponse.getStatus());
|
||||
JacksonUtil.writeValue(response.getWriter(), errorResponse);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@Getter
|
||||
public class JCPPException extends Exception {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JCPPErrorCode errorCode;
|
||||
|
||||
public JCPPException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public JCPPException(JCPPErrorCode errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public JCPPException(String message, JCPPErrorCode errorCode) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public JCPPException(String message, Throwable cause, JCPPErrorCode errorCode) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public JCPPException(Throwable cause, JCPPErrorCode errorCode) {
|
||||
super(cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.initializing;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.AuthorityEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.UserStatusEnum;
|
||||
import sanbing.jcpp.app.dal.entity.Gun;
|
||||
import sanbing.jcpp.app.dal.entity.Pile;
|
||||
import sanbing.jcpp.app.dal.entity.Station;
|
||||
import sanbing.jcpp.app.dal.entity.User;
|
||||
import sanbing.jcpp.app.dal.mapper.GunMapper;
|
||||
import sanbing.jcpp.app.dal.mapper.PileMapper;
|
||||
import sanbing.jcpp.app.dal.mapper.StationMapper;
|
||||
import sanbing.jcpp.app.dal.mapper.UserMapper;
|
||||
import sanbing.jcpp.app.data.InstallModeEnum;
|
||||
import sanbing.jcpp.app.data.kv.*;
|
||||
import sanbing.jcpp.app.service.AttributeService;
|
||||
import sanbing.jcpp.app.service.security.model.UserCredentials;
|
||||
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 数据库安装组件
|
||||
* 在Spring容器初始化时执行数据库操作
|
||||
* 如果失败会阻止应用启动,确保数据库环境正确
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Order(0)
|
||||
public class InstallInitializingBean implements InitializingBean {
|
||||
|
||||
/**
|
||||
* 安装模式
|
||||
* init - 初始化数据库并加载演示数据
|
||||
* upgrade - 升级数据库
|
||||
* disabled - 不执行任何操作
|
||||
*/
|
||||
@Value("${install.mode:disabled}")
|
||||
private String mode;
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
// Mappers for data insertion
|
||||
private final UserMapper userMapper;
|
||||
private final StationMapper stationMapper;
|
||||
private final PileMapper pileMapper;
|
||||
private final GunMapper gunMapper;
|
||||
|
||||
// Services for demo data
|
||||
private final AttributeService attributeService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (isDisabled()) {
|
||||
log.info("数据库安装功能已禁用,跳过安装操作");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
performInstallation();
|
||||
log.info("数据库安装操作完成");
|
||||
} catch (Exception e) {
|
||||
log.error("数据库安装操作失败,应用启动终止", e);
|
||||
// 抛出异常阻止Spring容器启动
|
||||
throw new RuntimeException("数据库初始化失败,应用无法启动", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDisabled() {
|
||||
return "disabled".equals(mode) || mode == null || mode.isEmpty();
|
||||
}
|
||||
|
||||
private void performInstallation() {
|
||||
InstallModeEnum installMode = InstallModeEnum.fromMode(mode);
|
||||
log.info("开始执行数据库安装操作,模式: {}", installMode.getDescription());
|
||||
|
||||
switch (installMode) {
|
||||
case INIT:
|
||||
doInitDatabase();
|
||||
break;
|
||||
case UPGRADE:
|
||||
doUpgradeDatabase();
|
||||
break;
|
||||
case DISABLED:
|
||||
log.info("数据库安装功能已禁用");
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的安装模式: {}", mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际执行数据库初始化的内部方法
|
||||
* 包括创建表结构和加载演示数据
|
||||
*/
|
||||
private void doInitDatabase() {
|
||||
log.info("开始初始化数据库...");
|
||||
|
||||
try {
|
||||
// 1. 执行数据库架构初始化脚本
|
||||
String schemaScript = loadResourceFile("sql/schema-init.sql");
|
||||
log.info("执行数据库架构初始化脚本");
|
||||
jdbcTemplate.execute(schemaScript);
|
||||
log.info("数据库架构初始化完成");
|
||||
|
||||
// 2. 加载演示数据
|
||||
log.info("开始加载演示数据...");
|
||||
doLoadDemoData();
|
||||
log.info("演示数据加载完成");
|
||||
|
||||
log.info("数据库初始化完成");
|
||||
} catch (Exception e) {
|
||||
log.error("数据库初始化失败", e);
|
||||
throw new RuntimeException("数据库初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级数据库
|
||||
*/
|
||||
private void doUpgradeDatabase() {
|
||||
log.info("开始升级数据库...");
|
||||
// TODO: 实现升级逻辑
|
||||
log.info("数据库升级完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载演示数据的内部方法
|
||||
*/
|
||||
private void doLoadDemoData() {
|
||||
log.info("开始加载演示数据...");
|
||||
|
||||
try {
|
||||
// 创建系统管理员账号
|
||||
createAdminUserIfNotExists();
|
||||
|
||||
// 创建5个演示充电站
|
||||
createDemoStationsIfNotExists();
|
||||
|
||||
log.info("演示数据加载完成");
|
||||
} catch (Exception e) {
|
||||
log.error("加载演示数据失败", e);
|
||||
throw new RuntimeException("加载演示数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String loadResourceFile(String path) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource(path);
|
||||
byte[] binaryData = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
return new String(binaryData, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void createAdminUserIfNotExists() {
|
||||
try {
|
||||
// 检查是否已存在同名用户(大小写不敏感)
|
||||
int userCount = userMapper.countByUserName("sanbing");
|
||||
|
||||
if (userCount == 0) {
|
||||
// 使用固定的UUID作为管理员用户ID
|
||||
UUID adminUserId = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
|
||||
|
||||
// 创建UserCredentials对象
|
||||
UserCredentials credentials = new UserCredentials();
|
||||
// 使用BCrypt加密密码
|
||||
String encodedPassword = passwordEncoder.encode("sanbing@123456");
|
||||
credentials.setPassword(encodedPassword);
|
||||
credentials.setEnabled(true);
|
||||
credentials.setFailedLoginAttempts(0);
|
||||
|
||||
User adminUser = User.builder()
|
||||
.id(adminUserId)
|
||||
.createdTime(LocalDateTime.now())
|
||||
.updatedTime(LocalDateTime.now())
|
||||
.additionalInfo(additionalInfo)
|
||||
.status(UserStatusEnum.ENABLE)
|
||||
.userName("sanbing")
|
||||
.userCredentials(credentials)
|
||||
.authority(AuthorityEnum.SYS_ADMIN) // 设置为系统管理员权限
|
||||
.version(1)
|
||||
.build();
|
||||
|
||||
userMapper.insert(adminUser);
|
||||
log.info("创建系统管理员账号: {}, 权限: {}", adminUser.getUserName(), adminUser.getAuthority());
|
||||
} else {
|
||||
log.info("系统管理员账号已存在,跳过创建");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("创建系统管理员账号失败", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建5个演示充电站,每个站配置不同数量的充电桩和枪
|
||||
*/
|
||||
private void createDemoStationsIfNotExists() {
|
||||
String[][] stationData = {
|
||||
{"07d80c81-fe99-4a1f-a6aa-dc4d798b5626", "三丙家专属充电站", "S20241001001", "120.1079330444336", "30.267013549804688", "浙江省", "杭州市", "西湖区", "西溪路552-1号"},
|
||||
{"17d80c81-fe99-4a1f-a6aa-dc4d798b5627", "西湖区政府充电站", "S20241001002", "120.1279330444336", "30.277013549804688", "浙江省", "杭州市", "西湖区", "文三路168号"},
|
||||
{"27d80c81-fe99-4a1f-a6aa-dc4d798b5628", "杭州大厦充电站", "S20241001003", "120.1679330444336", "30.257013549804688", "浙江省", "杭州市", "下城区", "延安路385号"},
|
||||
{"37d80c81-fe99-4a1f-a6aa-dc4d798b5629", "钱江新城充电站", "S20241001004", "120.1879330444336", "30.247013549804688", "浙江省", "杭州市", "江干区", "富春路701号"},
|
||||
{"47d80c81-fe99-4a1f-a6aa-dc4d798b562a", "滨江高新充电站", "S20241001005", "120.1979330444336", "30.207013549804688", "浙江省", "杭州市", "滨江区", "江南大道588号"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < stationData.length; i++) {
|
||||
Station station = createStationIfNotExists(stationData[i]);
|
||||
|
||||
// 为每个充电站创建充电桩和充电枪
|
||||
// 站1: 6桩10枪, 站2: 6桩10枪, 站3: 6桩10枪, 站4: 6桩10枪, 站5: 6桩10枪 = 30桩50枪
|
||||
createDemoPilesAndGunsForStation(station, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private Station createStationIfNotExists(String[] data) {
|
||||
try {
|
||||
UUID stationId = UUID.fromString(data[0]);
|
||||
Station existingStation = stationMapper.selectById(stationId);
|
||||
|
||||
if (existingStation == null) {
|
||||
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
|
||||
|
||||
Station station = Station.builder()
|
||||
.id(stationId)
|
||||
.createdTime(LocalDateTime.now())
|
||||
.updatedTime(LocalDateTime.now())
|
||||
.additionalInfo(additionalInfo)
|
||||
.stationName(data[1])
|
||||
.stationCode(data[2])
|
||||
.longitude(Float.parseFloat(data[3]))
|
||||
.latitude(Float.parseFloat(data[4]))
|
||||
.province(data[5])
|
||||
.city(data[6])
|
||||
.county(data[7])
|
||||
.address(data[8])
|
||||
.version(1)
|
||||
.build();
|
||||
|
||||
stationMapper.insert(station);
|
||||
log.info("创建演示电站: {}", station.getStationName());
|
||||
return station;
|
||||
} else {
|
||||
log.info("演示电站已存在,跳过创建: {}", data[1]);
|
||||
return existingStation;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("创建演示电站失败: {}", data[1], e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为每个充电站创建充电桩和充电枪
|
||||
* 总计: 5站 x 6桩 = 30桩, 每桩1-2枪 = 50枪
|
||||
*/
|
||||
private void createDemoPilesAndGunsForStation(Station station, int stationIndex) {
|
||||
// 每个充电站创建6个充电桩
|
||||
for (int pileIndex = 1; pileIndex <= 6; pileIndex++) {
|
||||
// 计算全局桩号:(站序号-1) * 6 + 桩序号,从20231212000001开始
|
||||
int globalPileNumber = (stationIndex - 1) * 6 + pileIndex;
|
||||
Pile pile = createDemoPileForStation(station, stationIndex, pileIndex, globalPileNumber);
|
||||
|
||||
// 为充电桩创建充电枪
|
||||
// 前4个充电桩每个2枪,后2个充电桩每个1枪,这样每站正好10枪
|
||||
int gunsPerPile = (pileIndex <= 4) ? 2 : 1;
|
||||
createDemoGunsForPile(pile, station, stationIndex, pileIndex, gunsPerPile);
|
||||
}
|
||||
}
|
||||
|
||||
private Pile createDemoPileForStation(Station station, int stationIndex, int pileIndex, int globalPileNumber) {
|
||||
// 生成唯一的UUID
|
||||
String uuidString = String.format("%08d-0000-4000-8000-%012d",
|
||||
stationIndex * 1000 + pileIndex, stationIndex * 1000000L + pileIndex);
|
||||
UUID pileId = UUID.fromString(uuidString);
|
||||
|
||||
Pile existingPile = pileMapper.selectById(pileId);
|
||||
if (existingPile != null) {
|
||||
log.info("充电桩已存在,跳过创建: {}", existingPile.getPileName());
|
||||
return existingPile;
|
||||
}
|
||||
|
||||
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
|
||||
|
||||
// 充电桩品牌和型号多样化
|
||||
String[] brands = {"星星", "特来电", "云快充", "国家电网", "南方电网", "蔚来"};
|
||||
String[] models = {"10A", "20A", "30A", "40A", "60A", "120A"};
|
||||
String brand = brands[((stationIndex - 1) * 6 + pileIndex - 1) % brands.length];
|
||||
String model = models[((stationIndex - 1) * 6 + pileIndex - 1) % models.length];
|
||||
|
||||
// 交流桩和直流桩混合
|
||||
PileTypeEnum pileType = (pileIndex % 3 == 0) ? PileTypeEnum.DC : PileTypeEnum.AC;
|
||||
|
||||
// 生成桩编号:从20231212000001开始递增
|
||||
String pileCode = String.format("20231212%06d", globalPileNumber);
|
||||
|
||||
Pile pile = Pile.builder()
|
||||
.id(pileId)
|
||||
.createdTime(LocalDateTime.now())
|
||||
.updatedTime(LocalDateTime.now())
|
||||
.additionalInfo(additionalInfo)
|
||||
.pileName(String.format("%s-%d号充电桩", station.getStationName(), pileIndex))
|
||||
.pileCode(pileCode)
|
||||
.protocol("yunkuaichongV150")
|
||||
.stationId(station.getId())
|
||||
.brand(brand)
|
||||
.model(model)
|
||||
.manufacturer(brand)
|
||||
.type(pileType)
|
||||
.version(1)
|
||||
.build();
|
||||
|
||||
pileMapper.insert(pile);
|
||||
log.info("创建演示充电桩: {}", pile.getPileName());
|
||||
|
||||
// 为新创建的充电桩插入演示属性(模拟在线/离线状态)
|
||||
loadDemoPileAttributes(pileId, stationIndex, pileIndex);
|
||||
|
||||
return pile;
|
||||
}
|
||||
|
||||
private void createDemoGunsForPile(Pile pile, Station station, int stationIndex, int pileIndex, int gunCount) {
|
||||
for (int gunIndex = 1; gunIndex <= gunCount; gunIndex++) {
|
||||
// 生成唯一的UUID
|
||||
String uuidString = String.format("%08d-1111-4000-8000-%012d",
|
||||
stationIndex * 10000 + pileIndex * 10 + gunIndex,
|
||||
stationIndex * 10000000L + pileIndex * 100000L + gunIndex);
|
||||
UUID gunId = UUID.fromString(uuidString);
|
||||
|
||||
Gun existingGun = gunMapper.selectById(gunId);
|
||||
if (existingGun != null) {
|
||||
log.info("充电枪已存在,跳过创建: {}", existingGun.getGunName());
|
||||
continue;
|
||||
}
|
||||
|
||||
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
|
||||
|
||||
Gun gun = Gun.builder()
|
||||
.id(gunId)
|
||||
.createdTime(LocalDateTime.now())
|
||||
.updatedTime(LocalDateTime.now())
|
||||
.additionalInfo(additionalInfo)
|
||||
.gunNo(String.format("%02d", gunIndex))
|
||||
.gunName(String.format("%s-%d号枪", pile.getPileName(), gunIndex))
|
||||
.gunCode(String.format("%s-%02d", pile.getPileCode(), gunIndex))
|
||||
.stationId(station.getId())
|
||||
.pileId(pile.getId())
|
||||
.version(1)
|
||||
.build();
|
||||
|
||||
gunMapper.insert(gun);
|
||||
log.info("创建演示充电枪: {}", gun.getGunName());
|
||||
|
||||
// 为新创建的充电枪插入演示属性(模拟不同运行状态)
|
||||
loadDemoGunAttributes(gunId, stationIndex, pileIndex, gunIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 为充电桩加载演示属性,模拟在线/离线状态
|
||||
*/
|
||||
private void loadDemoPileAttributes(UUID pileId, int stationIndex, int pileIndex) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// 模拟80%在线率,20%离线率
|
||||
boolean isOnline = ((stationIndex + pileIndex) % 5) != 0; // 80%在线
|
||||
String status = isOnline ? "ONLINE" : "OFFLINE";
|
||||
|
||||
// 插入状态属性
|
||||
AttributeKvEntry statusAttr = new BaseAttributeKvEntry(
|
||||
new StringDataEntry(AttrKeyEnum.STATUS.getCode(), status),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(pileId, statusAttr);
|
||||
|
||||
if (isOnline) {
|
||||
// 在线桩设置连接时间
|
||||
AttributeKvEntry connectedAtAttr = new BaseAttributeKvEntry(
|
||||
new LongDataEntry(AttrKeyEnum.CONNECTED_AT.getCode(), currentTime - (3600000L * (pileIndex % 12))),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(pileId, connectedAtAttr);
|
||||
} else {
|
||||
// 离线桩设置断开时间
|
||||
AttributeKvEntry disconnectedAtAttr = new BaseAttributeKvEntry(
|
||||
new LongDataEntry(AttrKeyEnum.DISCONNECTED_AT.getCode(), currentTime - (1800000L * (pileIndex % 6))),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(pileId, disconnectedAtAttr);
|
||||
}
|
||||
|
||||
log.info("为充电桩 {} 设置演示状态属性: {}", pileId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为充电枪加载演示属性,模拟多种运行状态
|
||||
*/
|
||||
private void loadDemoGunAttributes(UUID gunId, int stationIndex, int pileIndex, int gunIndex) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// 模拟九种充电枪运行状态的分布
|
||||
String[] gunStatuses = {
|
||||
"IDLE", // 空闲 - 30%
|
||||
"IDLE",
|
||||
"IDLE",
|
||||
"CHARGING", // 充电中 - 25%
|
||||
"CHARGING",
|
||||
"INSERTED", // 已插枪 - 15%
|
||||
"CHARGE_COMPLETE", // 充电完成 - 10%
|
||||
"FAULT", // 故障 - 8%
|
||||
"RESERVED", // 预约 - 5%
|
||||
"DISCHARGING", // 放电中 - 3%
|
||||
"DISCHARGE_READY", // 放电准备 - 2%
|
||||
"DISCHARGE_COMPLETE" // 放电完成 - 2%
|
||||
};
|
||||
|
||||
// 基于索引选择状态,确保有良好的分布
|
||||
int statusIndex = ((stationIndex - 1) * 20 + (pileIndex - 1) * 3 + (gunIndex - 1)) % gunStatuses.length;
|
||||
String gunStatus = gunStatuses[statusIndex];
|
||||
|
||||
// 插入枪运行状态属性
|
||||
AttributeKvEntry statusAttr = new BaseAttributeKvEntry(
|
||||
new StringDataEntry(AttrKeyEnum.GUN_RUN_STATUS.getCode(), gunStatus),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(gunId, statusAttr);
|
||||
|
||||
// 根据状态设置额外属性
|
||||
if ("CHARGING".equals(gunStatus) || "DISCHARGING".equals(gunStatus)) {
|
||||
// 充电中或放电中的枪设置功率
|
||||
double power = 10.0 + (statusIndex % 8) * 5.0; // 10-45kW
|
||||
AttributeKvEntry powerAttr = new BaseAttributeKvEntry(
|
||||
new DoubleDataEntry("chargingPower", power),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(gunId, powerAttr);
|
||||
}
|
||||
|
||||
if ("FAULT".equals(gunStatus)) {
|
||||
// 故障枪设置故障代码
|
||||
String[] faultCodes = {"E001", "E002", "E003", "E004", "E005"};
|
||||
String faultCode = faultCodes[statusIndex % faultCodes.length];
|
||||
AttributeKvEntry faultAttr = new BaseAttributeKvEntry(
|
||||
new StringDataEntry("faultCode", faultCode),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(gunId, faultAttr);
|
||||
}
|
||||
|
||||
log.info("为充电枪 {} 设置演示状态属性: {}", gunId, gunStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.initializing;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum;
|
||||
import sanbing.jcpp.app.dal.entity.Attribute;
|
||||
import sanbing.jcpp.app.dal.mapper.AttributeMapper;
|
||||
import sanbing.jcpp.app.data.PileSession;
|
||||
import sanbing.jcpp.app.data.kv.*;
|
||||
import sanbing.jcpp.app.data.page.PageDataIterable;
|
||||
import sanbing.jcpp.app.service.AttributeService;
|
||||
import sanbing.jcpp.app.service.PileService;
|
||||
import sanbing.jcpp.app.service.cache.session.PileSessionCacheKey;
|
||||
import sanbing.jcpp.infrastructure.cache.CacheValueWrapper;
|
||||
import sanbing.jcpp.infrastructure.cache.TransactionalCache;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 状态清洗组件
|
||||
* 在Spring容器初始化时执行充电桩状态的全量清洗
|
||||
* 如果失败会阻止应用启动,确保数据状态一致性
|
||||
*
|
||||
* @author baigod
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Order(10) // 在数据库初始化之后执行
|
||||
public class StatusCleanupInitializingBean implements InitializingBean {
|
||||
|
||||
private final PileService pileService;
|
||||
private final TransactionalCache<PileSessionCacheKey, PileSession> pileSessionCache;
|
||||
private final AttributeMapper attributeMapper;
|
||||
private final AttributeService attributeService;
|
||||
|
||||
@Value("${service.protocol.sessions.default-inactivity-timeout-in-sec:600}")
|
||||
private int inactivityTimeoutInSec;
|
||||
|
||||
// 分页大小,控制每次处理的充电桩数量
|
||||
private static final int FETCH_PAGE_SIZE = 1000;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
log.info("开始执行状态清洗...");
|
||||
|
||||
try {
|
||||
performStatusCleanup();
|
||||
log.info("状态清洗执行完成");
|
||||
} catch (Exception e) {
|
||||
log.error("状态清洗执行失败,应用启动终止", e);
|
||||
// 抛出异常阻止Spring容器启动,确保数据状态一致性
|
||||
throw new RuntimeException("系统状态清理失败,应用无法在数据状态不一致的情况下启动", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行充电桩状态清洗任务。
|
||||
*
|
||||
* 该方法的主要功能是检查所有充电桩的状态,并根据预定义的逻辑进行必要的更新,以确保数据库和缓存的状态一致性。
|
||||
*
|
||||
* 核心逻辑:
|
||||
* 1. 数据库状态为准,缓存为辅助判断。
|
||||
* 2. 有会话连接 = 在线,无会话连接 = 可能离线。
|
||||
* 3. 确保数据库和缓存的最终一致性。
|
||||
*
|
||||
* 处理场景:
|
||||
* - 场景1:有会话连接,但数据库状态为OFFLINE -> 更新为ONLINE。
|
||||
* - 场景2:无会话连接,但数据库状态为ONLINE -> 检查最后活跃时间,超时则设为OFFLINE。
|
||||
* - 场景3:无会话连接,数据库状态也为OFFLINE -> 跳过。
|
||||
* - 场景4:有会话连接,数据库状态也为ONLINE -> 跳过。
|
||||
*
|
||||
* 异常处理:
|
||||
* - 网络分区:依赖最后活跃时间判断。
|
||||
* - 系统重启:会话丢失,通过重连恢复状态。
|
||||
* - 缓存异常:以数据库状态为准。
|
||||
* - 数据库异常:记录错误,继续处理其他设备。
|
||||
*
|
||||
* @throws RuntimeException 如果在执行状态清洗过程中发生不可恢复的异常。
|
||||
*/
|
||||
private void performStatusCleanup() {
|
||||
log.info("开始执行充电桩状态清洗...");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
int processedCount = 0; // 已处理的充电桩数量
|
||||
int updatedCount = 0; // 状态已更新的充电桩数量
|
||||
int onlineCount = 0; // 最终在线的充电桩数量
|
||||
int offlineCount = 0; // 最终离线的充电桩数量
|
||||
|
||||
// 计算不活跃阈值时间,用于判断是否超时
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeoutThreshold = currentTime - (inactivityTimeoutInSec * 1000L);
|
||||
|
||||
try {
|
||||
// 使用分页查询所有充电桩,避免一次性加载过多数据导致内存溢出
|
||||
PageDataIterable<sanbing.jcpp.app.dal.entity.Pile> pileIterable = new PageDataIterable<>(
|
||||
pileService::findPilesWithPagination,
|
||||
FETCH_PAGE_SIZE
|
||||
);
|
||||
|
||||
for (var pile : pileIterable) {
|
||||
processedCount++;
|
||||
String pileCode = pile.getPileCode();
|
||||
|
||||
try {
|
||||
// 获取当前数据库中的状态
|
||||
String currentDbStatus = pileService.findPileStatus(pile.getId());
|
||||
boolean isCurrentlyOnline = PileStatusEnum.ONLINE.name().equals(currentDbStatus);
|
||||
|
||||
// 检查是否有活跃的会话连接
|
||||
boolean hasActiveSession = checkActiveSession(pileCode);
|
||||
|
||||
// 根据会话状态、数据库状态和超时时间决定目标状态
|
||||
String targetStatus = determineTargetStatus(hasActiveSession, isCurrentlyOnline, pile.getId(), timeoutThreshold);
|
||||
|
||||
// 如果需要更新状态,则执行更新操作
|
||||
if (!targetStatus.equals(currentDbStatus)) {
|
||||
updatePileStatusWithTimestamp(pile.getId(), targetStatus, currentTime);
|
||||
log.info("更新充电桩状态: pileCode={}, 从 {} 更新为 {}, 会话状态={}",
|
||||
pileCode, currentDbStatus, targetStatus, hasActiveSession ? "有" : "无");
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
// 统计最终状态
|
||||
if (PileStatusEnum.ONLINE.name().equals(targetStatus)) {
|
||||
onlineCount++;
|
||||
} else {
|
||||
offlineCount++;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 捕获单个充电桩处理过程中的异常,记录日志并继续处理其他充电桩
|
||||
log.error("处理充电桩状态清洗失败: pileCode={}", pileCode, e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 捕获全局异常,记录日志并抛出运行时异常
|
||||
log.error("执行状态清洗过程中发生异常", e);
|
||||
throw new RuntimeException("状态清洗执行失败", e);
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
// 记录状态清洗的汇总信息,包括处理数量、更新数量、在线数量、离线数量和耗时
|
||||
log.info("充电桩状态清洗完成: 处理数量={}, 更新数量={}, 在线数量={}, 离线数量={}, 耗时={}ms",
|
||||
processedCount, updatedCount, onlineCount, offlineCount, endTime - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查充电桩是否有活跃的会话连接
|
||||
*/
|
||||
private boolean checkActiveSession(String pileCode) {
|
||||
try {
|
||||
CacheValueWrapper<PileSession> sessionWrapper = pileSessionCache.get(new PileSessionCacheKey(pileCode));
|
||||
return sessionWrapper != null && sessionWrapper.get() != null;
|
||||
} catch (Exception e) {
|
||||
log.warn("检查充电桩会话失败: pileCode={}", pileCode, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话状态、数据库状态和超时时间决定目标状态
|
||||
*/
|
||||
private String determineTargetStatus(boolean hasActiveSession, boolean isCurrentlyOnline, UUID pileId, long timeoutThreshold) {
|
||||
// 有活跃会话,应该在线
|
||||
if (hasActiveSession) {
|
||||
return PileStatusEnum.ONLINE.name();
|
||||
}
|
||||
|
||||
// 无活跃会话,需要检查最后活跃时间
|
||||
if (isCurrentlyOnline) {
|
||||
// 当前显示在线但无会话,检查最后活跃时间
|
||||
Attribute lastActiveAttr = attributeMapper.findByEntityAndKey(pileId, AttrKeyEnum.LAST_ACTIVE_TIME.getCode());
|
||||
if (lastActiveAttr != null && lastActiveAttr.getLongV() != null) {
|
||||
long lastActiveTime = lastActiveAttr.getLongV();
|
||||
if (lastActiveTime < timeoutThreshold) {
|
||||
// 超时了,应该设为离线
|
||||
log.debug("充电桩超时未活跃,设为离线: pileId={}, lastActiveTime={}, threshold={}",
|
||||
pileId, lastActiveTime, timeoutThreshold);
|
||||
return PileStatusEnum.OFFLINE.name();
|
||||
}
|
||||
} else {
|
||||
// 没有最后活跃时间记录,但当前显示在线且无会话,保守地设为离线
|
||||
log.debug("充电桩无最后活跃时间记录但当前在线且无会话,设为离线: pileId={}", pileId);
|
||||
return PileStatusEnum.OFFLINE.name();
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况保持当前状态
|
||||
return isCurrentlyOnline ? PileStatusEnum.ONLINE.name() : PileStatusEnum.OFFLINE.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新充电桩状态,包括时间戳
|
||||
*/
|
||||
private void updatePileStatusWithTimestamp(UUID pileId, String status, long currentTime) {
|
||||
try {
|
||||
// 更新状态属性
|
||||
AttributeKvEntry statusAttr = new BaseAttributeKvEntry(
|
||||
new StringDataEntry(AttrKeyEnum.STATUS.getCode(), status),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(pileId, statusAttr);
|
||||
|
||||
// 根据状态更新相应的时间戳
|
||||
if (PileStatusEnum.ONLINE.name().equals(status)) {
|
||||
// 设为在线时更新连接时间和最后活跃时间
|
||||
updatePileAttribute(pileId, AttrKeyEnum.CONNECTED_AT, currentTime);
|
||||
updatePileAttribute(pileId, AttrKeyEnum.LAST_ACTIVE_TIME, currentTime);
|
||||
} else if (PileStatusEnum.OFFLINE.name().equals(status)) {
|
||||
// 设为离线时更新断开连接时间
|
||||
updatePileAttribute(pileId, AttrKeyEnum.DISCONNECTED_AT, currentTime);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("更新充电桩状态失败: pileId={}, status={}", pileId, status, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新充电桩特定属性
|
||||
*/
|
||||
private void updatePileAttribute(UUID pileId, AttrKeyEnum key, long value) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
AttributeKvEntry attr = new BaseAttributeKvEntry(
|
||||
new LongDataEntry(key.getCode(), value),
|
||||
currentTime
|
||||
);
|
||||
attributeService.save(pileId, attr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.service;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import sanbing.jcpp.app.data.kv.AttributeKvEntry;
|
||||
import sanbing.jcpp.app.data.kv.AttributesSaveResult;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AttributeService {
|
||||
|
||||
ListenableFuture<Optional<AttributeKvEntry>> find(UUID entityId, String attrKey);
|
||||
|
||||
ListenableFuture<List<AttributeKvEntry>> find(UUID entityId, Collection<String> attrKeys);
|
||||
|
||||
ListenableFuture<List<AttributeKvEntry>> findAll(UUID entityId);
|
||||
|
||||
ListenableFuture<AttributesSaveResult> save(UUID entityId, List<AttributeKvEntry> attributes);
|
||||
|
||||
ListenableFuture<AttributesSaveResult> save(UUID entityId, AttributeKvEntry attribute);
|
||||
|
||||
ListenableFuture<List<String>> removeAll(UUID entityId, List<String> attrKeys);
|
||||
|
||||
int removeAllByEntityId(UUID entityId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.service;
|
||||
|
||||
import sanbing.jcpp.app.adapter.response.DashboardStats;
|
||||
|
||||
/**
|
||||
* 仪表盘服务接口
|
||||
*
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface DashboardService {
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
DashboardStats getDashboardStats();
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import sanbing.jcpp.protocol.adapter.DownlinkController;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class DownlinkCallService {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.service;
|
||||
|
||||
import sanbing.jcpp.app.adapter.request.GunCreateRequest;
|
||||
import sanbing.jcpp.app.adapter.request.GunQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.request.GunUpdateRequest;
|
||||
import sanbing.jcpp.app.adapter.response.GunWithStatusResponse;
|
||||
import sanbing.jcpp.app.adapter.response.PageResponse;
|
||||
import sanbing.jcpp.app.dal.entity.Gun;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.GunRunStatus;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GunService {
|
||||
|
||||
/**
|
||||
* 创建充电枪
|
||||
*/
|
||||
Gun createGun(GunCreateRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID查询充电枪
|
||||
*/
|
||||
Gun findById(UUID id);
|
||||
|
||||
/**
|
||||
* 更新充电枪
|
||||
*/
|
||||
Gun updateGun(UUID id, GunUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除充电枪
|
||||
*/
|
||||
void deleteGun(UUID id);
|
||||
|
||||
/**
|
||||
* 分页查询充电枪及状态信息
|
||||
*/
|
||||
PageResponse<GunWithStatusResponse> queryGunsWithStatus(GunQueryRequest request);
|
||||
|
||||
/**
|
||||
* 根据充电桩编码和充电枪编码查询充电枪
|
||||
*/
|
||||
Gun findByPileCodeAndGunCode(String pileCode, String gunCode);
|
||||
|
||||
/**
|
||||
* 查询充电枪状态
|
||||
*
|
||||
* @param gunId 充电枪ID
|
||||
* @return 状态字符串,如果不存在返回null
|
||||
*/
|
||||
String findGunStatus(UUID gunId);
|
||||
|
||||
/**
|
||||
* 保存充电枪状态变更时序数据 - 高性能版本
|
||||
*
|
||||
* @param gunId 充电枪ID
|
||||
* @param status 状态
|
||||
* @param ts 时间戳,如果为null则使用当前时间
|
||||
*/
|
||||
void saveGunStatusChange(UUID gunId, String status, Long ts);
|
||||
|
||||
/**
|
||||
* 处理充电枪状态上报
|
||||
*
|
||||
* @param pileCode 充电桩编码
|
||||
* @param gunCode 充电枪编码
|
||||
* @param protoStatus Proto状态
|
||||
* @param ts 时间戳
|
||||
* @return 是否需要更新充电桩状态
|
||||
*/
|
||||
boolean handleGunRunStatus(String pileCode, String gunCode, GunRunStatus protoStatus, long ts);
|
||||
|
||||
}
|
||||
@@ -7,17 +7,13 @@
|
||||
package sanbing.jcpp.app.service;
|
||||
|
||||
import sanbing.jcpp.infrastructure.queue.Callback;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.OfflineCardBalanceUpdateRequest;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.OfflineCardSyncRequest;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.SetPricingRequest;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
|
||||
import sanbing.jcpp.proto.gen.ProtocolProto.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
* @author 九筒
|
||||
*/
|
||||
public interface PileProtocolService {
|
||||
/**
|
||||
@@ -30,6 +26,11 @@ public interface PileProtocolService {
|
||||
*/
|
||||
void heartBeat(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
/**
|
||||
* 处理会话关闭事件
|
||||
*/
|
||||
void onSessionCloseEvent(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
/**
|
||||
* 校验计费模型
|
||||
*/
|
||||
@@ -58,8 +59,6 @@ public interface PileProtocolService {
|
||||
/**
|
||||
* 远程启动反馈
|
||||
*
|
||||
* @param uplinkQueueMessage
|
||||
* @param callback
|
||||
*/
|
||||
void onRemoteStartChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
@@ -77,8 +76,8 @@ public interface PileProtocolService {
|
||||
* 启动充电(支持卡号和并充序号)
|
||||
* 当 parallelNo 不为空时,自动使用并充启机命令
|
||||
*/
|
||||
void startCharge(String pileCode, String gunCode, BigDecimal limitYuan, String orderNo,
|
||||
String logicalCardNo, String physicalCardNo, String parallelNo);
|
||||
void startCharge(String pileCode, String gunCode, BigDecimal limitYuan, String orderNo,
|
||||
String logicalCardNo, String physicalCardNo, String parallelNo);
|
||||
|
||||
/**
|
||||
* 停止充电
|
||||
@@ -121,9 +120,9 @@ public interface PileProtocolService {
|
||||
void postBmsAbort(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
/**
|
||||
* 远程更新
|
||||
* 远程更新
|
||||
*/
|
||||
void otaRequest(ProtocolProto.OtaRequest request);
|
||||
void otaRequest(OtaRequest request);
|
||||
|
||||
/**
|
||||
* 远程更新应答
|
||||
@@ -176,4 +175,9 @@ public interface PileProtocolService {
|
||||
*/
|
||||
void onTimeSyncResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
/**
|
||||
* 充电过程BMS需求与充电机输出
|
||||
*/
|
||||
void postBmsDemandChargerOutput(UplinkQueueMessage uplinkQueueMessage, Callback callback);
|
||||
|
||||
}
|
||||
118
jcpp-app/src/main/java/sanbing/jcpp/app/service/PileService.java
Normal file
118
jcpp-app/src/main/java/sanbing/jcpp/app/service/PileService.java
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
||||
* 微信:mohan_88888
|
||||
* 抖音:程序员三丙
|
||||
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
||||
*/
|
||||
package sanbing.jcpp.app.service;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import sanbing.jcpp.app.adapter.request.PileCreateRequest;
|
||||
import sanbing.jcpp.app.adapter.request.PileQueryRequest;
|
||||
import sanbing.jcpp.app.adapter.request.PileUpdateRequest;
|
||||
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.config.ibatis.enums.PileStatusEnum;
|
||||
import sanbing.jcpp.app.dal.entity.Pile;
|
||||
import sanbing.jcpp.app.data.kv.AttributesSaveResult;
|
||||
import sanbing.jcpp.app.exception.JCPPException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PileService {
|
||||
|
||||
/**
|
||||
* 创建充电桩
|
||||
*/
|
||||
Pile createPile(PileCreateRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID查询充电桩
|
||||
*/
|
||||
Pile findById(UUID id);
|
||||
|
||||
/**
|
||||
* 更新充电桩
|
||||
*/
|
||||
Pile updatePile(UUID id, PileUpdateRequest request) throws JCPPException;
|
||||
|
||||
/**
|
||||
* 删除充电桩
|
||||
*/
|
||||
void deletePile(UUID id) throws JCPPException;
|
||||
|
||||
/**
|
||||
* 分页查询充电桩及状态信息
|
||||
*/
|
||||
PageResponse<PileWithStatusResponse> queryPilesWithStatus(PileQueryRequest request);
|
||||
|
||||
/**
|
||||
* 获取充电桩选项列表
|
||||
*/
|
||||
List<PileOptionResponse> getPileOptions();
|
||||
|
||||
/**
|
||||
* 更新充电桩状态
|
||||
*
|
||||
* @param pileId 充电桩ID
|
||||
* @param status 新状态
|
||||
*/
|
||||
void updatePileStatus(UUID pileId, PileStatusEnum status);
|
||||
|
||||
/**
|
||||
* 根据充电桩编码更新状态
|
||||
*
|
||||
* @param pileCode 充电桩编码
|
||||
* @param status 新状态
|
||||
*/
|
||||
void updatePileStatusByCode(String pileCode, PileStatusEnum status);
|
||||
|
||||
/**
|
||||
* 查询所有充电桩
|
||||
*/
|
||||
List<Pile> findAll();
|
||||
|
||||
/**
|
||||
* 分页查询充电桩(用于状态清洗等批处理场景)
|
||||
*
|
||||
* @param offset 偏移量
|
||||
* @param limit 限制数量
|
||||
* @return 充电桩列表
|
||||
*/
|
||||
List<Pile> findPilesWithPagination(int offset, int limit);
|
||||
|
||||
/**
|
||||
* 查询充电桩状态
|
||||
*
|
||||
* @param pileId 充电桩ID
|
||||
* @return 状态字符串,如果不存在返回null
|
||||
*/
|
||||
String findPileStatus(UUID pileId);
|
||||
|
||||
/**
|
||||
* 处理充电桩登录后的状态管理(优化版)
|
||||
* 执行:更新STATUS为ONLINE → 更新CONNECTED_AT → 更新LAST_ACTIVE_TIME
|
||||
*
|
||||
* @param pileId 充电桩ID
|
||||
* @return 异步操作结果
|
||||
*/
|
||||
ListenableFuture<AttributesSaveResult> handlePileLogin(UUID pileId);
|
||||
|
||||
/**
|
||||
* 处理充电桩心跳时的状态管理(优化版)
|
||||
* 执行:更新STATUS为ONLINE → 更新LAST_ACTIVE_TIME
|
||||
*
|
||||
* @param pileId 充电桩ID
|
||||
* @return 异步操作结果
|
||||
*/
|
||||
ListenableFuture<AttributesSaveResult> handlePileHeartbeat(UUID pileId);
|
||||
|
||||
/**
|
||||
* 处理充电桩会话关闭时的状态管理
|
||||
*
|
||||
* @param pileCode 充电桩编码
|
||||
*/
|
||||
void handlePileSessionClose(String pileCode);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user