* !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,48 @@
#
# 开源代码,仅供学习和交流研究使用,商用请联系三丙
# 微信mohan_88888
# 抖音:程序员三丙
# 付费课程知识星球https://t.zsxq.com/aKtXo
#
#-------------------------------------------------------------------------------#
# Discover additional configuration options in our documentation #
# https://www.jetbrains.com/help/qodana/github.html #
#-------------------------------------------------------------------------------#
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches:
- master
- Feat_web_ui
jobs:
qodana:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2025.2
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
with:
args: --baseline,qodana.sarif.json
# In pr-mode: 'true' Qodana checks only changed files
pr-mode: false
use-caches: true
post-pr-comment: true
use-annotations: true
# Upload Qodana results (SARIF, other artifacts, logs) as an artifact to the job
upload-result: false
# quick-fixes available in Ultimate and Ultimate Plus plans
push-fixes: 'none'

View File

@@ -9,7 +9,7 @@
FROM registry.cn-hangzhou.aliyuncs.com/sanbing/jcpp-base:1.0 AS base
WORKDIR /app
COPY . .
RUN mvn -U -B -T 0.8C clean install -DskipTests
RUN mvn -U -B -T 4 clean package -DskipTests
#分层
FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-bullseye AS builder
@@ -37,7 +37,7 @@ RUN chmod a+x start.sh \
&& chmod 700 -R /home/sanbing/logs/* \
&& chown -R sanbing:sanbing /home/sanbing
EXPOSE 8080 8080
EXPOSE 8080/tcp 38001/tcp 38002/tcp 38003/tcp 38011/tcp
ENV APP_LOG_LEVEL=INFO
ENV PROTOCOLS_LOG_LEVEL=INFO

View File

@@ -6,9 +6,9 @@
#
FROM registry.cn-hangzhou.aliyuncs.com/sanbing/mvn:3.9.9-jdk21 AS base
FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-bullseye AS base
WORKDIR /app
COPY . .
RUN mvn -U -B -T 0.8C clean install -DskipTests \
RUN mvn -U -B -T 4 clean compile -DskipTests \
&& rm -rf /app

View File

@@ -7,7 +7,7 @@
volumes:
postgresql_data: {}
jcpp_pg_data: {}
networks:
sanbing-network:
@@ -33,5 +33,4 @@ services:
- 'POSTGRESQL_DEFAULT_TRANSACTION_ISOLATION=read committed'
- 'TZ=Asia/Shanghai'
volumes:
- postgresql_data:/bitnami/postgresql
- ./schema/schema-postgres.sql:/docker-entrypoint-initdb.d/init.sql
- jcpp_pg_data:/bitnami/postgresql

View File

@@ -37,7 +37,7 @@ RUN chmod a+x start.sh \
&& chmod 700 -R /home/sanbing/logs/* \
&& chown -R sanbing:sanbing /home/sanbing
EXPOSE 8080 8080
EXPOSE 8080/tcp 38001/tcp 38002/tcp 38003/tcp 38011/tcp
ENV PROTOCOLS_LOG_LEVEL=INFO

View File

@@ -1,124 +0,0 @@
--
-- 开源代码,仅供学习和交流研究使用,商用请联系三丙
-- 微信mohan_88888
-- 抖音:程序员三丙
-- 付费课程知识星球https://t.zsxq.com/aKtXo
--
CREATE TABLE IF NOT EXISTS jcpp_user
(
id uuid not null
constraint owner_pkey
primary key,
created_time timestamp default CURRENT_TIMESTAMP not null,
additional_info jsonb,
status varchar(16) not null,
user_name varchar(255) not null,
user_credentials jsonb not null,
version int default 1
);
CREATE UNIQUE INDEX IF NOT EXISTS uni_user_name
on jcpp_user (user_name);
CREATE TABLE IF NOT EXISTS jcpp_station
(
id uuid not null
constraint station_pkey
primary key,
created_time timestamp default CURRENT_TIMESTAMP not null,
additional_info jsonb,
station_name varchar(255) not null,
station_code varchar(255) not null,
owner_id uuid not null,
longitude double precision not null,
latitude double precision not null,
owner_type varchar(16) not null,
province varchar(255),
city varchar(255),
county varchar(255),
address varchar(255),
status varchar(16) not null,
version int default 1
);
CREATE UNIQUE INDEX IF NOT EXISTS uni_station_code
on jcpp_station (station_code);
CREATE TABLE IF NOT EXISTS jcpp_pile
(
id uuid not null
constraint pile_pkey
primary key,
created_time timestamp default CURRENT_TIMESTAMP not null,
additional_info jsonb,
pile_name varchar(255) not null,
pile_code varchar(255) not null,
protocol varchar(255) not null,
station_id uuid not null,
owner_id uuid not null,
owner_type varchar(16) not null,
brand varchar(255),
model varchar(255),
manufacturer varchar(255),
status varchar(16) not null,
type varchar(16) not null,
version int default 1
);
CREATE UNIQUE INDEX IF NOT EXISTS uni_pile_code
on jcpp_pile (pile_code);
CREATE TABLE IF NOT EXISTS jcpp_gun
(
id uuid not null
primary key,
created_time timestamp default CURRENT_TIMESTAMP not null,
additional_info varchar(255),
gun_no varchar(255) not null,
gun_name varchar(255) not null,
gun_code varchar(255) not null,
station_id uuid not null,
pile_id uuid not null,
owner_id uuid not null,
owner_type varchar(16) not null,
run_status varchar(16) not null,
run_status_updated_time timestamp not null,
opt_status varchar(16) not null,
version int default 1
);
CREATE UNIQUE INDEX IF NOT EXISTS uni_gun_code
on jcpp_gun (gun_code);
CREATE TABLE IF NOT EXISTS jcpp_order
(
id uuid not null
primary key,
internal_order_no varchar(255) not null,
external_order_no varchar(255) not null,
pile_order_No varchar(255) not null,
created_time timestamp default CURRENT_TIMESTAMP not null,
additional_info jsonb,
updated_time timestamp,
cancelled_time timestamp,
status varchar(16) not null,
type varchar(16) not null,
creator_id uuid not null,
station_id uuid not null,
pile_id uuid not null,
gun_id uuid not null,
plate_no varchar(64),
settlement_amount numeric(16, 8) default 0 not null,
settlement_details jsonb,
electricity_quantity numeric(16, 8) default 0 not null
);
CREATE UNIQUE INDEX IF NOT EXISTS uni_internal_order_no
on jcpp_order (internal_order_no);
CREATE UNIQUE INDEX IF NOT EXISTS uni_external_order_no
on jcpp_order (external_order_no);

View File

@@ -33,6 +33,12 @@
<groupId>sanbing</groupId>
<artifactId>jcpp-app</artifactId>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-web-ui</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-protocol-yunkuaichong</artifactId>
@@ -54,24 +60,6 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
<layout>ZIP</layout>
<mainClass>sanbing.jcpp.JCPPServerApplication</mainClass>
<excludeDevtools>true</excludeDevtools>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
开源代码,仅供学习和交流研究使用,商用请联系三丙
微信mohan_88888
抖音:程序员三丙
付费课程知识星球https://t.zsxq.com/aKtXo
-->
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.7.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="application">
<includeModuleDependencies />
</into>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>

View File

@@ -6,26 +6,34 @@
*/
package sanbing.jcpp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.Ordered;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import sanbing.jcpp.infrastructure.util.annotation.AfterStartUp;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author baigod
* @author 九筒
*/
@SpringBootApplication
@EnableAsync
@EnableScheduling
@Slf4j
public class JCPPServerApplication {
private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "app-service";
private static long startTs;
public static void main(String[] args) {
startTs = System.currentTimeMillis();
new SpringApplicationBuilder(JCPPServerApplication.class).bannerMode(Banner.Mode.LOG).run(updateArguments(args));
}
@@ -38,4 +46,10 @@ public class JCPPServerApplication {
}
return args;
}
@AfterStartUp(order = Ordered.LOWEST_PRECEDENCE)
public void afterStartUp() {
long startupTimeMs = System.currentTimeMillis() - startTs;
log.info("Started JChargePointProtocol App Service in {} seconds", TimeUnit.MILLISECONDS.toSeconds(startupTimeMs));
}
}

View File

@@ -1,4 +1,4 @@
server:
server:
address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
port: "${HTTP_BIND_PORT:8080}"
undertow:
@@ -16,6 +16,10 @@
options:
server:
record-request-start-time: true
servlet:
encoding:
charset: UTF-8
force: true
spring:
application:
@@ -29,6 +33,50 @@ spring:
leak-detection-threshold: "${SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}"
maximum-pool-size: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:64}"
register-mbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
servlet:
multipart:
max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}"
max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}"
main:
allow-circular-references: "true"
mvc:
pathmatch.matching-strategy: "ANT_PATH_MATCHER"
async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}"
cors:
mappings:
"[/api/**]":
allowed-origin-patterns: "*"
allowed-methods: "*"
allowed-headers: "*"
max-age: "1800"
allow-credentials: "true"
web:
resources:
chain:
compressed: "true"
strategy:
content:
enabled: "true"
security:
jwt:
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}"
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}"
tokenIssuer: "${JWT_TOKEN_ISSUER:sanbing.io}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:NUI3T1FCSVI0MFNweFAxRjEyUUJ1QkVPc3V0T1hEdUJON0hiZ2NydzdzN0RBSVZwTE9rMFFWeDVTRnZ6djMxSw==}"
# 安全设置配置
settings:
# 密码策略
passwordPolicy:
minimumLength: "${SECURITY_PASSWORD_POLICY_MINIMUM_LENGTH:6}"
maximumLength: "${SECURITY_PASSWORD_POLICY_MAXIMUM_LENGTH:72}"
minimumUppercaseLetters: "${SECURITY_PASSWORD_POLICY_MINIMUM_UPPERCASE_LETTERS:0}"
minimumLowercaseLetters: "${SECURITY_PASSWORD_POLICY_MINIMUM_LOWERCASE_LETTERS:0}"
minimumDigits: "${SECURITY_PASSWORD_POLICY_MINIMUM_DIGITS:0}"
minimumSpecialCharacters: "${SECURITY_PASSWORD_POLICY_MINIMUM_SPECIAL_CHARACTERS:0}"
allowWhitespaces: "${SECURITY_PASSWORD_POLICY_ALLOW_WHITESPACES:true}"
# 最大登录失败次数
maxFailedLoginAttempts: "${SECURITY_MAX_FAILED_LOGIN_ATTEMPTS:5}"
mybatis-plus:
type-handlers-package: sanbing.jcpp.app.dal.config.ibatis.typehandlers
@@ -111,17 +159,25 @@ queue:
stats:
enabled: "${QUEUE_APP_STATS_ENABLED:true}"
print-interval-ms: "${QUEUE_APP_STATS_PRINT_INTERVAL_MS:60000}"
timer-top-n: "${QUEUE_APP_STATS_TIMER_TOP_N:5}"
# 应用程序缓存配置
cache:
type: "${CACHE_TYPE:caffeine}" # caffeine or redis
maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}"
specs:
piles:
timeToLiveInMinutes: "${CACHE_SPECS_PILES_TTL:1440}"
maxSize: "${CACHE_SPECS_PILES_MAX_SIZE:100000}"
guns:
timeToLiveInMinutes: "${CACHE_SPECS_GUNS_TTL:1440}"
maxSize: "${CACHE_SPECS_GUNS_MAX_SIZE:1000000}"
pileSessions:
timeToLiveInMinutes: "${CACHE_SPECS_PILE_SESSIONS_TTL:1440}"
timeToLiveInMinutes: "${service.protocol.sessions.default-inactivity-timeout-in-sec}"
maxSize: "${CACHE_SPECS_PILE_SESSIONS_MAX_SIZE:100000}"
attributes:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}"
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}"
redis:
connection:
@@ -133,7 +189,6 @@ redis:
clientName: "${REDIS_CLIENT_NAME:standalone}"
commandTimeout: "${REDIS_CLIENT_COMMAND_TIMEOUT:30000}"
shutdownTimeout: "${REDIS_CLIENT_SHUTDOWN_TIMEOUT:1000}"
readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}"
usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:true}"
cluster:
nodes: "${REDIS_NODES:redis-node-0:6379,redis-node-1:6379,redis-node-2:6379,redis-node-3:6379,redis-node-4:6379,redis-node-5:6379}"
@@ -160,6 +215,29 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
evictTtlInMs: "${REDIS_EVICT_TTL_MS:60000}"
# 数据库安装配置
install:
# 安装模式init-初始化数据库upgrade-升级disabled-不做任何操作
mode: "${INSTALL_MODE:disabled}"
# SQL相关配置
sql:
attributes:
# 批处理大小
batch_size: "${SQL_ATTRIBUTES_BATCH_SIZE:1000}"
# 批处理最大延迟(毫秒)
batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY:100}"
# 统计打印间隔(毫秒)
stats_print_interval_ms: "${SQL_ATTRIBUTES_STATS_PRINT_INTERVAL_MS:1000}"
# 批处理线程数
batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:4}"
# 值是否进行XSS验证
value_no_xss_validation: "${SQL_ATTRIBUTES_VALUE_NO_XSS_VALIDATION:false}"
# 批处理排序
batch_sort: "${SQL_BATCH_SORT:true}"
# 是否移除空字符
remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
service:
# 服务类型:纯协议解析前置 - protocol纯应用后端 - app单体服务(包含protocol和app) - monolith
type: "${SERVICE_TYPE:monolith}"

View File

@@ -1,4 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
开源代码,仅供学习和交流研究使用,商用请联系三丙
微信mohan_88888
抖音:程序员三丙
付费课程知识星球https://t.zsxq.com/aKtXo
-->
<configuration status="INFO" monitorInterval="30">
<properties>

View File

@@ -14,7 +14,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/**
* @author baigod
* @author 九筒
*/
@ActiveProfiles("test")
@SpringBootTest(classes = JCPPServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

View File

@@ -10,9 +10,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import sanbing.jcpp.AbstractTestBase;
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.app.dal.entity.Gun;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
@@ -22,10 +19,10 @@ import java.util.UUID;
import static sanbing.jcpp.app.dal.mapper.PileMapperIT.NORMAL_PILE_ID;
import static sanbing.jcpp.app.dal.mapper.StationMapperIT.NORMAL_STATION_ID;
import static sanbing.jcpp.app.dal.mapper.UserMapperIT.NORMAL_USER_ID;
/**
* @author baigod
* @author 九筒
*/
class GunMapperIT extends AbstractTestBase {
static final UUID[] NORMAL_GUN_ID = new UUID[]{
@@ -61,11 +58,6 @@ class GunMapperIT extends AbstractTestBase {
.gunCode("202312120000" + new DecimalFormat("00").format(i + 1) + "-02")
.stationId(NORMAL_STATION_ID)
.pileId(pileId)
.ownerId(NORMAL_USER_ID)
.ownerType(OwnerTypeEnum.C)
.runStatus(GunRunStatusEnum.IDLE)
.runStatusUpdatedTime(LocalDateTime.now())
.optStatus(GunOptStatusEnum.AVAILABLE)
.build();
gunMapper.insertOrUpdate(gun);

View File

@@ -1,67 +0,0 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.app.dal.mapper;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;
import sanbing.jcpp.AbstractTestBase;
import sanbing.jcpp.app.dal.config.ibatis.enums.OrderStatusEnum;
import sanbing.jcpp.app.dal.config.ibatis.enums.OrderTypeEnum;
import sanbing.jcpp.app.dal.entity.Order;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import static sanbing.jcpp.app.dal.mapper.GunMapperIT.NORMAL_GUN_ID;
import static sanbing.jcpp.app.dal.mapper.PileMapperIT.NORMAL_PILE_ID;
import static sanbing.jcpp.app.dal.mapper.StationMapperIT.NORMAL_STATION_ID;
import static sanbing.jcpp.app.dal.mapper.UserMapperIT.NORMAL_USER_ID;
/**
* @author baigod
*/
class OrderMapperIT extends AbstractTestBase {
@Resource
OrderMapper orderMapper;
@Test
void testOrderMapper() {
orderMapper.delete(Wrappers.lambdaQuery());
Order order = Order.builder()
.id(UUID.randomUUID())
.internalOrderNo(IdUtil.getSnowflake(1, 1).nextIdStr())
.externalOrderNo(IdUtil.getSnowflake(1, 1).nextIdStr())
.pileOrderNo(RandomStringUtils.secure().nextNumeric(16))
.createdTime(LocalDateTime.now())
.additionalInfo(JacksonUtil.newObjectNode())
.updatedTime(LocalDateTime.now())
.cancelledTime(null)
.status(OrderStatusEnum.IN_CHARGING)
.type(OrderTypeEnum.CHARGE)
.creatorId(NORMAL_USER_ID)
.stationId(NORMAL_STATION_ID)
.pileId(NORMAL_PILE_ID[0])
.gunId(NORMAL_GUN_ID[0])
.plateNo("浙A88888")
.settlementAmount(new BigDecimal(100))
.settlementDetails(JacksonUtil.newObjectNode())
.electricityQuantity(new BigDecimal("100"))
.build();
orderMapper.insertOrUpdate(order);
log.info("{}", orderMapper.selectById(order.getId()));
}
}

View File

@@ -11,8 +11,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import sanbing.jcpp.AbstractTestBase;
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.app.dal.entity.Pile;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
@@ -25,10 +23,10 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static sanbing.jcpp.app.dal.mapper.StationMapperIT.NORMAL_STATION_ID;
import static sanbing.jcpp.app.dal.mapper.UserMapperIT.NORMAL_USER_ID;
/**
* @author baigod
* @author 九筒
*/
class PileMapperIT extends AbstractTestBase {
static final UUID[] NORMAL_PILE_ID = new UUID[]{
@@ -61,12 +59,9 @@ class PileMapperIT extends AbstractTestBase {
.pileCode("202312120000" + new DecimalFormat("00").format(i + 1))
.protocol("yunkuaichongV150")
.stationId(NORMAL_STATION_ID)
.ownerId(NORMAL_USER_ID)
.ownerType(OwnerTypeEnum.C)
.brand("星星")
.model("10A")
.manufacturer("星星")
.status(PileStatusEnum.IDLE)
.type(PileTypeEnum.AC)
.build();
@@ -90,12 +85,9 @@ class PileMapperIT extends AbstractTestBase {
.pileCode("20241015" + new DecimalFormat("000000").format(number.get()))
.protocol("yunkuaichongV150")
.stationId(NORMAL_STATION_ID)
.ownerId(NORMAL_USER_ID)
.ownerType(OwnerTypeEnum.C)
.brand("星星")
.model("10A")
.manufacturer("星星")
.status(PileStatusEnum.IDLE)
.type(PileTypeEnum.AC)
.build();
piles.add(pile);

View File

@@ -10,18 +10,16 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import sanbing.jcpp.AbstractTestBase;
import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum;
import sanbing.jcpp.app.dal.config.ibatis.enums.StationStatusEnum;
import sanbing.jcpp.app.dal.entity.Station;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import java.time.LocalDateTime;
import java.util.UUID;
import static sanbing.jcpp.app.dal.mapper.UserMapperIT.NORMAL_USER_ID;
/**
* @author baigod
* @author 九筒
*/
class StationMapperIT extends AbstractTestBase {
static final UUID NORMAL_STATION_ID = UUID.fromString("07d80c81-fe99-4a1f-a6aa-dc4d798b5626");
@@ -39,15 +37,12 @@ class StationMapperIT extends AbstractTestBase {
.additionalInfo(JacksonUtil.newObjectNode())
.stationName("三丙家专属充电站")
.stationCode("S20241001001")
.ownerId(NORMAL_USER_ID)
.longitude(120.107936F)
.latitude(30.267014F)
.ownerType(OwnerTypeEnum.C)
.province("浙江省")
.city("杭州市")
.county("西湖区")
.address("西溪路552-1号")
.status(StationStatusEnum.OPERATIONAL)
.build();
stationMapper.insertOrUpdate(station);

View File

@@ -10,15 +10,17 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import sanbing.jcpp.AbstractTestBase;
import sanbing.jcpp.app.dal.config.ibatis.enums.AuthorityEnum;
import sanbing.jcpp.app.dal.config.ibatis.enums.UserStatusEnum;
import sanbing.jcpp.app.dal.entity.User;
import sanbing.jcpp.app.service.security.model.UserCredentials;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* @author baigod
* @author 九筒
*/
class UserMapperIT extends AbstractTestBase {
static final UUID NORMAL_USER_ID = UUID.fromString("21cbf909-a23a-4396-840a-f34061f59f95");
@@ -30,17 +32,33 @@ class UserMapperIT extends AbstractTestBase {
void curdTest() {
userMapper.delete(Wrappers.lambdaQuery());
// 创建UserCredentials对象
UserCredentials credentials = new UserCredentials();
credentials.setPassword("$2a$10$mE.qmcV0mFU5NcKh73TZx.z4ueI/.bDWbj0T1BYyqP481kGGarKLG"); // encoded "password123"
credentials.setEnabled(true);
credentials.setFailedLoginAttempts(0);
User user = User.builder()
.id(NORMAL_USER_ID)
.createdTime(LocalDateTime.now())
.additionalInfo(JacksonUtil.newObjectNode())
.status(UserStatusEnum.ENABLE)
.userName("sanbing")
.userCredentials(JacksonUtil.newObjectNode())
.userCredentials(credentials)
.authority(AuthorityEnum.SYS_ADMIN) // 添加权限字段
.version(1) // 添加版本字段
.build();
userMapper.insertOrUpdate(user);
log.info("{}", userMapper.selectById(NORMAL_USER_ID));
User savedUser = userMapper.selectById(NORMAL_USER_ID);
log.info("Saved user: {}", savedUser);
// 验证UserCredentials字段正确保存和读取
assert savedUser != null;
assert savedUser.getUserCredentials() != null;
assert savedUser.getUserCredentials().isEnabled();
assert "sanbing".equals(savedUser.getUserName());
assert AuthorityEnum.SYS_ADMIN.equals(savedUser.getAuthority());
}
}

View File

@@ -52,6 +52,60 @@
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
</dependency>
<dependency>
<groupId>com.github.ua-parser</groupId>
<artifactId>uap-java</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk18on</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Web for REST API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

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

Some files were not shown because too many files have changed in this diff Show More