* !44 comment
* !39 添加下行日志打印
* !36 扩展计价领域模型
* !35 webui 初步成型
* !34 webui 初步成型
This commit is contained in:
三丙
2025-09-09 08:23:59 +00:00
parent 921045af8f
commit 58580ca11e
372 changed files with 37900 additions and 1206 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("获取用户信息失败"));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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; // 详细地址
}

View File

@@ -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; // 关键字搜索(站名或编码)
}

View File

@@ -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; // 详细地址
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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, // 交流充电桩

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> {
}

View File

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

View File

@@ -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> {
}

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

@@ -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(), "最后更新时间戳错误:时间戳必须为正数");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 + '\'' +
'}';
}
}

View File

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

View File

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

View File

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

View File

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

View 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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ import sanbing.jcpp.protocol.adapter.DownlinkController;
import java.util.UUID;
/**
* @author baigod
* @author 九筒
*/
@Slf4j
public abstract class DownlinkCallService {

View File

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

View File

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

View 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