云快充1.5.0 初始化

This commit is contained in:
3god
2024-10-08 09:38:54 +08:00
parent dea6774942
commit cb19b45919
297 changed files with 18020 additions and 28 deletions

56
jcpp-app/pom.xml Normal file
View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
抖音关注:程序员三丙
知识星球https://t.zsxq.com/j9b21
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>sanbing</groupId>
<artifactId>jcpp-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jcpp-app</artifactId>
<packaging>jar</packaging>
<name>JChargePointProtocol App Module</name>
<description>应用模块</description>
<properties>
<main.dir>${basedir}/..</main.dir>
</properties>
<dependencies>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-protocol-api</artifactId>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-infrastructure-queue</artifactId>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-infrastructure-cache</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,76 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
@Configuration
@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class})
@MapperScan({"sanbing.jcpp.app.dal.mapper"})
public class DalConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Primary
@ConfigurationProperties(prefix = "spring.datasource.hikari")
@Bean
public DataSource dataSource(@Qualifier("dataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Primary
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Primary
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Primary
@Bean
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionTemplate.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
return transactionTemplate;
}
@Bean
@Primary
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,27 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
/**
* @author baigod
*/
public enum GunRunStatusEnum implements IEnum<String> {
IDLE, // 空闲
INSERTED, // 已插枪
CHARGING, // 充电中
CHARGE_COMPLETE, // 充电完成
DISCHARGE_READY, // 放电准备
DISCHARGING, // 放电中
DISCHARGE_COMPLETE, // 放电完成
RESERVED, // 预约
FAULT; // 故障
@Override
public String getValue() {
return name();
}
}

View File

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,21 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,21 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
/**
* @author baigod
*/
public enum PileStatusEnum implements IEnum<String> {
IDLE, // 空闲
WORKING, // 工作中
FAULT, // 故障
MAINTENANCE, // 维护中
OFFLINE, // 离线
;
@Override
public String getValue() {
return name();
}
}

View File

@@ -0,0 +1,21 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
/**
* @author baigod
*/
public enum PileTypeEnum implements IEnum<String> {
AC, // 交流充电桩
DC, // 直流充电桩
;
@Override
public String getValue() {
return name();
}
}

View File

@@ -0,0 +1,26 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,20 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
/**
* @author baigod
*/
public enum UserStatusEnum implements IEnum<String> {
ENABLE,
DISABLE;
@Override
public String getValue() {
return name();
}
}

View File

@@ -0,0 +1,40 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.typehandlers;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.postgresql.util.PGobject;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@Slf4j
@MappedTypes({JsonNode.class})
public class JsonbTypeHandler extends JacksonTypeHandler {
public JsonbTypeHandler(Class<?> type) {
super(type);
}
public JsonbTypeHandler(Class<?> type, Field field) {
super(type, field);
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
if (ps != null) {
PGobject jsonObject = new PGobject();
jsonObject.setType("jsonb");
jsonObject.setValue(JacksonUtil.toString(parameter));
ps.setObject(i, jsonObject);
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.config.ibatis.typehandlers;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
/**
* mysql UUID 类型转 varchar
*/
public class UUIDTypeHandler extends BaseTypeHandler<UUID> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException {
ps.setObject(i, parameter);
}
@Override
public UUID getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getObject(columnName, UUID.class);
}
@Override
public UUID getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getObject(columnIndex, UUID.class);
}
@Override
public UUID getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getObject(columnIndex, UUID.class);
}
}

View File

@@ -0,0 +1,61 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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.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 java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@TableName("jcpp_gun")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Gun implements Serializable, HasVersion {
@TableId(type = IdType.INPUT)
private UUID id;
private LocalDateTime createdTime;
private JsonNode additionalInfo;
private String gunNo;
private String gunName;
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

@@ -0,0 +1,69 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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 Long settlementAmount;
private JsonNode settlementDetails;
private BigDecimal electricityQuantity;
}

View File

@@ -0,0 +1,61 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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.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 java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@TableName(value = "jcpp_pile", autoResultMap = true)
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Pile implements Serializable, HasVersion {
@TableId(type = IdType.INPUT)
private UUID id;
private LocalDateTime createdTime;
private JsonNode additionalInfo;
private String pileName;
private String pileCode;
private String protocol;
private UUID stationId;
private UUID ownerId;
private OwnerTypeEnum ownerType;
private String brand;
private String model;
private String manufacturer;
private PileStatusEnum status;
private PileTypeEnum type;
private Integer version;
}

View File

@@ -0,0 +1,62 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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.OwnerTypeEnum;
import sanbing.jcpp.app.dal.config.ibatis.enums.StationStatusEnum;
import sanbing.jcpp.infrastructure.cache.HasVersion;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@TableName("jcpp_station")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Station implements Serializable, HasVersion {
@TableId(type = IdType.INPUT)
private UUID id;
private LocalDateTime createdTime;
private JsonNode additionalInfo;
private String stationName;
private String stationCode;
private UUID ownerId;
private Float longitude;
private Float latitude;
private OwnerTypeEnum ownerType;
private String province;
private String city;
private String county;
private String address;
private StationStatusEnum status;
private Integer version;
}

View File

@@ -0,0 +1,45 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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.UserStatusEnum;
import sanbing.jcpp.infrastructure.cache.HasVersion;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@TableName("jcpp_user")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable, HasVersion {
@TableId(type = IdType.INPUT)
private UUID id;
private LocalDateTime createdTime;
private JsonNode additionalInfo;
private UserStatusEnum status;
private String userName;
private JsonNode userCredentials;
private Integer version;
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import sanbing.jcpp.app.dal.entity.Gun;
/**
* @author baigod
*/
public interface GunMapper extends BaseMapper<Gun> {
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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

@@ -0,0 +1,23 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import sanbing.jcpp.app.dal.entity.Pile;
/**
* @author baigod
*/
public interface PileMapper extends BaseMapper<Pile> {
@Select("SELECT " +
" * " +
"FROM " +
" jcpp_pile " +
"WHERE " +
" pile_code = #{pileCode}")
Pile selectByCode(String pileCode);
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import sanbing.jcpp.app.dal.entity.Station;
/**
* @author baigod
*/
public interface StationMapper extends BaseMapper<Station> {
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import sanbing.jcpp.app.dal.entity.User;
/**
* @author baigod
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,57 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.data;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.UUID;
/**
* @author baigod
*/
@Data
public class PileSession implements Serializable {
private final UUID pileId;
private final String pileCode;
private final String protocolName;
private UUID protocolSessionId;
private String remoteAddress;
private String nodeId;
private String nodeWebapiIpPort;
public PileSession(UUID pileId, String pileCode, String protocolName) {
this.pileId = pileId;
this.pileCode = pileCode;
this.protocolName = protocolName;
}
@JsonCreator
public PileSession(
@JsonProperty("pileId") UUID pileId,
@JsonProperty("pileCode") String pileCode,
@JsonProperty("protocolName") String protocolName,
@JsonProperty("protocolSessionId") UUID protocolSessionId,
@JsonProperty("remoteAddress") String remoteAddress,
@JsonProperty("nodeId") String nodeId,
@JsonProperty("nodeWebapiIpPort") String nodeWebapiIpPort) {
this.pileId = pileId;
this.pileCode = pileCode;
this.protocolName = protocolName;
this.protocolSessionId = protocolSessionId;
this.remoteAddress = remoteAddress;
this.nodeId = nodeId;
this.nodeWebapiIpPort = nodeWebapiIpPort;
}
}

View File

@@ -0,0 +1,23 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.repository;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.io.Serializable;
public abstract class AbstractCachedEntityRepository<K extends Serializable, V extends Serializable, E> extends AbstractEntityRepository {
protected void publishEvictEvent(E event) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
eventPublisher.publishEvent(event);
} else {
handleEvictEvent(event);
}
}
public abstract void handleEvictEvent(E event);
}

View File

@@ -0,0 +1,17 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.repository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
@Slf4j
public abstract class AbstractEntityRepository {
@Resource
protected ApplicationEventPublisher eventPublisher;
}

View File

@@ -0,0 +1,19 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.repository;
import jakarta.annotation.Resource;
import sanbing.jcpp.infrastructure.cache.HasVersion;
import sanbing.jcpp.infrastructure.cache.VersionedCache;
import sanbing.jcpp.infrastructure.cache.VersionedCacheKey;
import java.io.Serializable;
public abstract class CachedVersionedEntityRepository<K extends VersionedCacheKey, V extends Serializable & HasVersion, E> extends AbstractCachedEntityRepository<K, V, E> {
@Resource
protected VersionedCache<K, V> cache;
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.repository;
import sanbing.jcpp.app.dal.entity.Pile;
/**
* @author baigod
*/
public interface PileRepository {
Pile findPileByCode(String pileCode);
}

View File

@@ -0,0 +1,47 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.repository;
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.Pile;
import sanbing.jcpp.app.dal.mapper.PileMapper;
import sanbing.jcpp.app.service.cache.pile.PileCacheEvictEvent;
import sanbing.jcpp.app.service.cache.pile.PileCacheKey;
import java.util.ArrayList;
import java.util.List;
import static sanbing.jcpp.infrastructure.util.validation.Validator.validateString;
/**
* @author baigod
*/
@Repository
@Slf4j
public class PileRepositoryImpl extends CachedVersionedEntityRepository<PileCacheKey, Pile, PileCacheEvictEvent> implements PileRepository {
@Resource
PileMapper pileMapper;
@TransactionalEventListener(classes = PileCacheEvictEvent.class)
@Override
public void handleEvictEvent(PileCacheEvictEvent event) {
// 如果修改或删除充电桩,需要在这里消费删除事件
List<PileCacheKey> toEvict = new ArrayList<>(3);
toEvict.add(new PileCacheKey(event.getPileId()));
toEvict.add(new PileCacheKey(event.getPileCode()));
cache.evict(toEvict);
}
@Override
public Pile findPileByCode(String pileCode) {
validateString(pileCode, code -> "无效的桩编号" + pileCode);
return cache.get(new PileCacheKey(pileCode),
() -> pileMapper.selectByCode(pileCode));
}
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
/**
* @author baigod
*/
public interface DownlinkCallService {
void sendDownlinkMessage(DownlinkRestMessage.Builder downlinkMessageBuilder, String pileCode);
}

View File

@@ -0,0 +1,66 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service;
import sanbing.jcpp.infrastructure.queue.Callback;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
/**
* @author baigod
*/
public interface PileProtocolService {
/**
* 桩登录
*/
void pileLogin(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 充电桩心跳
*/
void heartBeat(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 校验计费模型
*/
void verifyPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 查询计费策略
*/
void queryPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 上报电桩运行状态
*/
void postGunRunStatus(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 上报充电进度
*/
void postChargingProgress(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 费率下发反馈
*/
void onSetPricingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 远程启动反馈
*
* @param uplinkQueueMessage
* @param callback
*/
void onRemoteStartChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 远程停止反馈
*/
void onRemoteStopChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback);
/**
* 交易记录上报
*/
void onTransactionRecord(UplinkQueueMessage uplinkQueueMessage, Callback callback);
}

View File

@@ -0,0 +1,17 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.pile;
import lombok.Data;
import java.util.UUID;
@Data
public class PileCacheEvictEvent {
private UUID pileId;
private String pileCode;
}

View File

@@ -0,0 +1,47 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.pile;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import sanbing.jcpp.infrastructure.cache.VersionedCacheKey;
import java.io.Serial;
import java.util.Optional;
import java.util.UUID;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class PileCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 6366389552842340207L;
private final UUID pileId;
private final String pileCode;
public PileCacheKey(UUID pileId) {
this(pileId, null);
}
public PileCacheKey(String pileCode) {
this(null, pileCode);
}
@Override
public String toString() {
return Optional.ofNullable(pileId).map(UUID::toString).orElse(pileCode);
}
@Override
public boolean isVersioned() {
return pileId != null;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.pile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.dal.entity.Pile;
import sanbing.jcpp.infrastructure.cache.CacheConstants;
import sanbing.jcpp.infrastructure.cache.VersionedCaffeineCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("PileCache")
public class PileCaffeineCache extends VersionedCaffeineCache<PileCacheKey, Pile> {
public PileCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.PILE_CACHE);
}
}

View File

@@ -0,0 +1,33 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.pile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.dal.entity.Pile;
import sanbing.jcpp.infrastructure.cache.*;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("PileCache")
public class PileRedisCache extends VersionedRedisCache<PileCacheKey, Pile> {
public PileRedisCache(JCPPRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory) {
super(CacheConstants.PILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new JCPPRedisSerializer<>() {
@Override
public byte[] serialize(Pile pile) throws SerializationException {
return JacksonUtil.writeValueAsBytes(pile);
}
@Override
public Pile deserialize(PileCacheKey key, byte[] bytes) throws SerializationException {
return JacksonUtil.fromBytes(bytes, Pile.class);
}
});
}
}

View File

@@ -0,0 +1,41 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.session;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.util.Optional;
import java.util.UUID;
/**
* @author baigod
*/
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class PileSessionCacheKey implements Serializable {
private final UUID pileId;
private final String pileCode;
public PileSessionCacheKey(UUID pileId) {
this(pileId, null);
}
public PileSessionCacheKey(String pileCode) {
this(null, pileCode);
}
@Override
public String toString() {
return Optional.ofNullable(pileId).map(UUID::toString).orElse(pileCode);
}
}

View File

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.session;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.data.PileSession;
import sanbing.jcpp.infrastructure.cache.CacheConstants;
import sanbing.jcpp.infrastructure.cache.CaffeineTransactionalCache;
/**
* @author baigod
*/
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("PileSessionCache")
public class PileSessionCaffeineCache extends CaffeineTransactionalCache<PileSessionCacheKey, PileSession> {
public PileSessionCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.PILE_SESSION_CACHE);
}
}

View File

@@ -0,0 +1,36 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.cache.session;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.data.PileSession;
import sanbing.jcpp.infrastructure.cache.*;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
/**
* @author baigod
*/
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("PileSessionCache")
public class PileSessionRedisCache extends RedisTransactionalCache<PileSessionCacheKey, PileSession> {
public PileSessionRedisCache(JCPPRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory) {
super(CacheConstants.PILE_SESSION_CACHE, cacheSpecsMap, connectionFactory, configuration, new JCPPRedisSerializer<>() {
@Override
public byte[] serialize(PileSession pileSession) throws SerializationException {
return JacksonUtil.writeValueAsBytes(pileSession);
}
@Override
public PileSession deserialize(PileSessionCacheKey key, byte[] bytes) throws SerializationException {
return JacksonUtil.fromBytes(bytes, PileSession.class);
}
});
}
}

View File

@@ -0,0 +1,32 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
/**
* @author baigod
*/
@Configuration
public class DownlinkRestTemplateConfiguration {
@Bean("downlinkRestTemplate")
public RestTemplate downlinkRestTemplate() {
RestTemplate restTemplate = new RestTemplateBuilder()
.setConnectTimeout(Duration.of(3, ChronoUnit.SECONDS))
.setReadTimeout(Duration.of(3, ChronoUnit.SECONDS))
.build();
restTemplate.setMessageConverters(Collections.singletonList(new ProtobufHttpMessageConverter()));
return restTemplate;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.impl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import sanbing.jcpp.app.data.PileSession;
import sanbing.jcpp.app.service.DownlinkCallService;
import sanbing.jcpp.app.service.cache.session.PileSessionCacheKey;
import sanbing.jcpp.infrastructure.cache.CacheValueWrapper;
import sanbing.jcpp.infrastructure.cache.TransactionalCache;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
import sanbing.jcpp.protocol.adapter.DownlinkController;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.*;
/**
* @author baigod
*/
@Service
@Slf4j
public class DefaultDownlinkCallService implements DownlinkCallService {
@Resource
RestTemplate downlinkRestTemplate;
@Resource
ServiceInfoProvider serviceInfoProvider;
@Resource
DownlinkController downlinkController;
@Resource
TransactionalCache<PileSessionCacheKey, PileSession> pileSessionCache;
@Override
public void sendDownlinkMessage(DownlinkRestMessage.Builder downlinkMessageBuilder, String pileCode) {
if (serviceInfoProvider.isMonolith()) {
downlinkController.onDownlink(downlinkMessageBuilder.build())
.setResultHandler(result -> log.info("下行消息发送完成"));
} else {
try {
CacheValueWrapper<PileSession> pileSessionCacheValueWrapper = pileSessionCache.get(new PileSessionCacheKey(pileCode));
if (pileSessionCacheValueWrapper == null) {
log.warn("充电桩会话不存在 {}", pileCode);
return;
}
PileSession pileSession = pileSessionCacheValueWrapper.get();
invokeDownlinkRestApi(downlinkMessageBuilder.build(), pileSession.getNodeWebapiIpPort());
} catch (RestClientException e) {
log.error("下行消息发送异常", e);
}
}
}
private void invokeDownlinkRestApi(DownlinkRestMessage downlinkRestMessage, String nodeWebapiIpPort) {
HttpHeaders headers = new HttpHeaders();
headers.add(JCPP_TRACER_ID, TracerContextUtil.getCurrentTracer().getTraceId());
headers.add(JCPP_TRACER_ORIGIN, TracerContextUtil.getCurrentTracer().getOrigin());
headers.add(JCPP_TRACER_TS, String.valueOf(TracerContextUtil.getCurrentTracer().getTracerTs()));
headers.setContentType(MediaType.parseMediaType("application/x-protobuf"));
HttpEntity<DownlinkRestMessage> entity = new HttpEntity<>(downlinkRestMessage, headers);
try {
ResponseEntity<?> response = downlinkRestTemplate.postForEntity("http://" + nodeWebapiIpPort + "/api/onDownlink",
entity, ResponseEntity.class);
log.info("下行消息发送成功 {}", response);
} catch (RestClientException e) {
log.error("下行消息发送失败 {}", downlinkRestMessage, e);
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,264 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.impl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.dal.entity.Pile;
import sanbing.jcpp.app.data.PileSession;
import sanbing.jcpp.app.repository.PileRepository;
import sanbing.jcpp.app.service.DownlinkCallService;
import sanbing.jcpp.app.service.PileProtocolService;
import sanbing.jcpp.app.service.cache.session.PileSessionCacheKey;
import sanbing.jcpp.infrastructure.cache.TransactionalCache;
import sanbing.jcpp.infrastructure.proto.ProtoConverter;
import sanbing.jcpp.infrastructure.proto.model.PricingModel;
import sanbing.jcpp.infrastructure.proto.model.PricingModel.FlagPrice;
import sanbing.jcpp.infrastructure.proto.model.PricingModel.Period;
import sanbing.jcpp.infrastructure.queue.Callback;
import sanbing.jcpp.proto.gen.ProtocolProto.*;
import sanbing.jcpp.protocol.domain.DownlinkCmdEnum;
import java.time.LocalTime;
import java.util.*;
import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelFlag.*;
import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelRule.SPLIT_TIME;
import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelType.CHARGE;
/**
* @author baigod
*/
@Service
@Slf4j
public class DefaultPileProtocolService implements PileProtocolService {
@Resource
PileRepository pileRepository;
@Resource
TransactionalCache<PileSessionCacheKey, PileSession> pileSessionCache;
@Resource
DownlinkCallService downlinkCallService;
@Override
public void pileLogin(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到桩登录事件 {}", uplinkQueueMessage);
LoginRequest loginRequest = uplinkQueueMessage.getLoginRequest();
Pile pile = pileRepository.findPileByCode(loginRequest.getPileCode());
String pileCode = loginRequest.getPileCode();
log.info("查询到充电桩信息 {}", pile);
// 构造下行回复
DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, loginRequest.getPileCode());
downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.LOGIN_ACK.name());
if (pile != null) {
// 保存到缓存
cacheSession(uplinkQueueMessage, pile,
loginRequest.getRemoteAddress(),
loginRequest.getNodeId(),
loginRequest.getNodeWebapiIpPort());
downlinkMessageBuilder.setLoginResponse(LoginResponse.newBuilder()
.setSuccess(true)
.setPileCode(loginRequest.getPileCode())
.build());
} else {
downlinkMessageBuilder.setLoginResponse(LoginResponse.newBuilder()
.setSuccess(false)
.setPileCode(loginRequest.getPileCode())
.build());
}
downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode);
callback.onSuccess();
}
@Override
public void heartBeat(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到桩心跳事件 {}", uplinkQueueMessage);
HeartBeatRequest heartBeatRequest = uplinkQueueMessage.getHeartBeatRequest();
Pile pile = pileRepository.findPileByCode(heartBeatRequest.getPileCode());
if (pile != null) {
// 重新保存到缓存
cacheSession(uplinkQueueMessage, pile,
heartBeatRequest.getRemoteAddress(),
heartBeatRequest.getNodeId(),
heartBeatRequest.getNodeWebapiIpPort());
}
}
private void cacheSession(UplinkQueueMessage uplinkQueueMessage, Pile pile, String remoteAddress, String nodeId, String nodeWebapiIpPort) {
PileSession pileSession = new PileSession(pile.getId(), pile.getPileCode(), uplinkQueueMessage.getProtocolName());
pileSession.setProtocolSessionId(new UUID(uplinkQueueMessage.getSessionIdMSB(), uplinkQueueMessage.getSessionIdLSB()));
pileSession.setRemoteAddress(remoteAddress);
pileSession.setNodeId(nodeId);
pileSession.setNodeWebapiIpPort(nodeWebapiIpPort);
pileSessionCache.put(new PileSessionCacheKey(pile.getId()), pileSession);
pileSessionCache.put(new PileSessionCacheKey(pile.getPileCode()), pileSession);
}
@Override
public void verifyPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到计费模型验证请求 {}", uplinkQueueMessage);
VerifyPricingRequest verifyPricingRequest = uplinkQueueMessage.getVerifyPricingRequest();
String pileCode = verifyPricingRequest.getPileCode();
long pricingId = verifyPricingRequest.getPricingId();
// todo 默认校验成功,后续查库校验
assert pricingId > 0;
DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode);
downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.VERIFY_PRICING_ACK.name());
downlinkMessageBuilder.setVerifyPricingResponse(VerifyPricingResponse.newBuilder()
.setSuccess(true)
.setPricingId(pricingId)
.build());
downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode);
callback.onSuccess();
}
@Override
public void queryPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩计费模型请求 {}", uplinkQueueMessage);
QueryPricingRequest queryPricingRequest = uplinkQueueMessage.getQueryPricingRequest();
String pileCode = queryPricingRequest.getPileCode();
// TODO 先构造一个通用的计费模型,后续根据业务做库查询
List<Period> periods = new ArrayList<>();
periods.add(createPeriod(1, LocalTime.parse("00:00"), LocalTime.parse("06:00"), TOP));
periods.add(createPeriod(2, LocalTime.parse("06:00"), LocalTime.parse("12:00"), PEAK));
periods.add(createPeriod(3, LocalTime.parse("12:00"), LocalTime.parse("18:00"), FLAT));
periods.add(createPeriod(4, LocalTime.parse("18:00"), LocalTime.parse("00:00"), VALLEY));
Map<PricingModelFlag, FlagPrice> flagPriceMap = new HashMap<>();
flagPriceMap.put(TOP, new FlagPrice(75, 45));
flagPriceMap.put(PEAK, new FlagPrice(75, 45));
flagPriceMap.put(FLAT, new FlagPrice(75, 45));
flagPriceMap.put(VALLEY, new FlagPrice(75, 45));
PricingModel model = new PricingModel();
model.setId(UUID.randomUUID());
model.setSequenceNumber(1);
model.setPileCode(pileCode);
model.setType(CHARGE);
model.setRule(SPLIT_TIME);
model.setStandardElec(75);
model.setStandardServ(45);
model.setFlagPriceList(flagPriceMap);
model.setPeriodsList(periods);
// 构造下行计费
DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode);
downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.QUERY_PRICING_ACK.name());
downlinkMessageBuilder.setQueryPricingResponse(QueryPricingResponse.newBuilder()
.setPileCode(pileCode)
.setPricingId(model.getSequenceNumber())
.setPricingModel(ProtoConverter.toPricingModel(model))
.build());
downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode);
callback.onSuccess();
}
@Override
public void postGunRunStatus(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩上报的电桩状态 {}", uplinkQueueMessage);
callback.onSuccess();
}
@Override
public void postChargingProgress(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩上报的充电进度 {}", uplinkQueueMessage);
callback.onSuccess();
}
@Override
public void onSetPricingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩上费率下发反馈 {}", uplinkQueueMessage);
callback.onSuccess();
}
@Override
public void onRemoteStartChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩启动结果反馈 {}", uplinkQueueMessage);
callback.onSuccess();
}
@Override
public void onRemoteStopChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩停止结果反馈 {}", uplinkQueueMessage);
callback.onSuccess();
}
@Override
public void onTransactionRecord(UplinkQueueMessage uplinkQueueMessage, Callback callback) {
log.info("接收到充电桩交易记录上报 {}", uplinkQueueMessage);
// todo 毛都不敢先给个回复
TransactionRecord transactionRecord = uplinkQueueMessage.getTransactionRecord();
String tradeNo = transactionRecord.getTradeNo();
String pileCode = transactionRecord.getPileCode();
// 构造下行计费
DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode);
downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.TRANSACTION_RECORD.name());
downlinkMessageBuilder.setTransactionRecordAck(TransactionRecordAck.newBuilder()
.setTradeNo(tradeNo)
.setSuccess(true)
.build());
downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode);
callback.onSuccess();
}
private static Period createPeriod(int sn, LocalTime beginTime, LocalTime endTime, PricingModelFlag flag) {
Period period = new Period();
period.setSn(sn);
period.setBegin(beginTime);
period.setEnd(endTime);
period.setFlag(flag);
return period;
}
private DownlinkRestMessage.Builder createDownlinkMessageBuilder(UplinkQueueMessage uplinkQueueMessage, String pileCode) {
UUID messageId = UUID.randomUUID();
DownlinkRestMessage.Builder builder = DownlinkRestMessage.newBuilder();
builder.setMessageIdMSB(messageId.getLeastSignificantBits());
builder.setMessageIdLSB(messageId.getLeastSignificantBits());
builder.setPileCode(pileCode);
builder.setSessionIdMSB(uplinkQueueMessage.getSessionIdMSB());
builder.setSessionIdLSB(uplinkQueueMessage.getSessionIdLSB());
builder.setProtocolName(uplinkQueueMessage.getProtocolName());
builder.setRequestIdMSB(uplinkQueueMessage.getMessageIdMSB());
builder.setRequestIdLSB(uplinkQueueMessage.getMessageIdLSB());
builder.setRequestData(uplinkQueueMessage.getRequestData());
return builder;
}
}

View File

@@ -0,0 +1,66 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.event.JCPPApplicationEventListener;
import sanbing.jcpp.infrastructure.queue.discovery.event.PartitionChangeEvent;
import sanbing.jcpp.infrastructure.util.annotation.AfterStartUp;
import sanbing.jcpp.infrastructure.util.async.JCPPExecutors;
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractConsumerService extends JCPPApplicationEventListener<PartitionChangeEvent> {
protected final PartitionProvider partitionProvider;
protected final ApplicationEventPublisher eventPublisher;
protected ExecutorService consumersExecutor;
protected ExecutorService mgmtExecutor;
protected ScheduledExecutorService scheduler;
public void init(String prefix) {
this.consumersExecutor = Executors.newCachedThreadPool(JCPPThreadFactory.forName(prefix + "-consumer"));
this.mgmtExecutor = JCPPExecutors.newWorkStealingPool(getMgmtThreadPoolSize(), prefix + "-mgmt");
this.scheduler = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName(prefix + "-consumer-scheduler"));
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void afterStartUp() {
startConsumers();
}
protected void startConsumers() {
}
protected void stopConsumers() {
}
protected abstract int getMgmtThreadPoolSize();
@PreDestroy
public void destroy() {
stopConsumers();
if (consumersExecutor != null) {
consumersExecutor.shutdownNow();
}
if (mgmtExecutor != null) {
mgmtExecutor.shutdownNow();
}
if (scheduler != null) {
scheduler.shutdownNow();
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.stats.StatsCounter;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class AppConsumerStats {
public static final String TOTAL_MSGS = "totalMsgs";
public static final String LOGIN_EVENTS = "loginEvents";
public static final String HEARTBEAT_EVENTS = "heartBeatEvents";
public static final String GUN_RUN_STATUS_EVENTS = "gunRunStatusEvents";
public static final String CHARGING_PROGRESS_EVENTS = "chargingProgressEvents";
public static final String TRANSACTION_RECORD_EVENTS = "transactionRecordEvents";
private final StatsCounter totalCounter;
private final StatsCounter loginCounter;
private final StatsCounter heartBeatCounter;
private final StatsCounter gunRunStatusCounter;
private final StatsCounter chargingProgressCounter;
private final StatsCounter transactionRecordCounter;
private final Timer appConsumerTimer;
private final List<StatsCounter> counters = new ArrayList<>();
public AppConsumerStats(StatsFactory statsFactory) {
String statsKey = "appConsumer";
this.totalCounter = register(statsFactory.createStatsCounter(statsKey, TOTAL_MSGS));
this.loginCounter = register(statsFactory.createStatsCounter(statsKey, LOGIN_EVENTS));
this.heartBeatCounter = register(statsFactory.createStatsCounter(statsKey, HEARTBEAT_EVENTS));
this.gunRunStatusCounter = register(statsFactory.createStatsCounter(statsKey, GUN_RUN_STATUS_EVENTS));
this.chargingProgressCounter = register(statsFactory.createStatsCounter(statsKey, CHARGING_PROGRESS_EVENTS));
this.transactionRecordCounter = register(statsFactory.createStatsCounter(statsKey, TRANSACTION_RECORD_EVENTS));
this.appConsumerTimer = statsFactory.createTimer(statsKey);
}
private StatsCounter register(StatsCounter counter) {
counters.add(counter);
return counter;
}
public void log(UplinkQueueMessage msg) {
totalCounter.increment();
if (msg.hasLoginRequest()) {
loginCounter.increment();
} else if (msg.hasHeartBeatRequest()) {
heartBeatCounter.increment();
} else if (msg.hasGunRunStatusProto()) {
gunRunStatusCounter.increment();
} else if (msg.hasChargingProgressProto()) {
chargingProgressCounter.increment();
} else if (msg.hasTransactionRecord()) {
transactionRecordCounter.increment();
}
appConsumerTimer.record(Duration.ofMillis(System.currentTimeMillis() - TracerContextUtil.getCurrentTracer().getTracerTs()));
}
public void printStats() {
int total = totalCounter.get();
if (total > 0) {
StringBuilder stats = new StringBuilder();
counters.forEach(counter -> {
stats.append(counter.getName()).append(" = [").append(counter.get()).append("] ");
});
log.info("App Queue Consumer Stats: {}", stats);
}
}
public void reset() {
counters.forEach(StatsCounter::clear);
}
}

View File

@@ -0,0 +1,296 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.queue.QueueConsumer;
import sanbing.jcpp.infrastructure.queue.QueueMsg;
import sanbing.jcpp.infrastructure.queue.common.QueueConfig;
import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo;
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
@Slf4j
public class AppQueueConsumerManager<M extends QueueMsg, C extends QueueConfig> {
protected final String queueName;
@Getter
protected C config;
protected final MsgPackProcessor<M, C> msgPackProcessor;
protected final BiFunction<C, Integer, QueueConsumer<M>> consumerCreator;
protected final ExecutorService consumerExecutor;
protected final ScheduledExecutorService scheduler;
protected final ExecutorService taskExecutor;
private final Queue<QueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>();
private final ReentrantLock lock = new ReentrantLock();
@Getter
private volatile Set<TopicPartitionInfo> partitions;
protected volatile ConsumerWrapper<M> consumerWrapper;
protected volatile boolean stopped;
@Builder
public AppQueueConsumerManager(String queueName, C config,
MsgPackProcessor<M, C> msgPackProcessor,
BiFunction<C, Integer, QueueConsumer<M>> consumerCreator,
ExecutorService consumerExecutor,
ScheduledExecutorService scheduler,
ExecutorService taskExecutor) {
this.queueName = queueName;
this.config = config;
this.msgPackProcessor = msgPackProcessor;
this.consumerCreator = consumerCreator;
this.consumerExecutor = consumerExecutor;
this.scheduler = scheduler;
this.taskExecutor = taskExecutor;
if (config != null) {
init(config);
}
}
public void init(C config) {
this.config = config;
if (config.isConsumerPerPartition()) {
this.consumerWrapper = new ConsumerPerPartitionWrapper();
} else {
this.consumerWrapper = new SingleConsumerWrapper();
}
log.debug("[{}] Initialized consumer for queue: {}", queueName, config);
}
public void update(C config) {
addTask(QueueConsumerManagerTask.configUpdate(config));
}
public void update(Set<TopicPartitionInfo> partitions) {
addTask(QueueConsumerManagerTask.partitionChange(partitions));
}
protected void addTask(QueueConsumerManagerTask todo) {
if (stopped) {
return;
}
tasks.add(todo);
log.info("[{}] Added task: {}", queueName, todo);
tryProcessTasks();
}
@SuppressWarnings("unchecked")
private void tryProcessTasks() {
taskExecutor.submit(() -> {
if (lock.tryLock()) {
try {
C newConfig = null;
Set<TopicPartitionInfo> newPartitions = null;
while (!stopped) {
QueueConsumerManagerTask task = tasks.poll();
if (task == null) {
break;
}
log.info("[{}] Processing task: {}", queueName, task);
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) {
newPartitions = task.getPartitions();
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
newConfig = (C) task.getConfig();
} else {
processTask(task);
}
}
if (stopped) {
return;
}
if (newConfig != null) {
doUpdate(newConfig);
}
if (newPartitions != null) {
doUpdate(newPartitions);
}
} catch (Exception e) {
log.error("[{}] Failed to process tasks", queueName, e);
} finally {
lock.unlock();
}
} else {
log.trace("[{}] Failed to acquire lock", queueName);
scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
}
});
}
protected void processTask(QueueConsumerManagerTask task) {
}
private void doUpdate(C newConfig) {
log.info("[{}] Processing queue update: {}", queueName, newConfig);
var oldConfig = this.config;
this.config = newConfig;
if (log.isTraceEnabled()) {
log.trace("[{}] Old queue configuration: {}", queueName, oldConfig);
log.trace("[{}] New queue configuration: {}", queueName, newConfig);
}
if (oldConfig == null) {
init(config);
} else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) {
consumerWrapper.getConsumers().forEach(QueueConsumerTask::initiateStop);
consumerWrapper.getConsumers().forEach(QueueConsumerTask::awaitCompletion);
init(config);
if (partitions != null) {
doUpdate(partitions);
}
} else {
log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueName);
}
}
private void doUpdate(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
consumerWrapper.updatePartitions(partitions);
}
private void launchConsumer(QueueConsumerTask<M> consumerTask) {
log.info("[{}] Launching consumer", consumerTask.getKey());
Future<?> consumerLoop = consumerExecutor.submit(() -> {
JCPPThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
try {
consumerLoop(consumerTask.getConsumer());
} catch (Throwable e) {
log.error("Failure in consumer loop", e);
}
log.info("[{}] Consumer stopped", consumerTask.getKey());
});
consumerTask.setTask(consumerLoop);
}
private void consumerLoop(QueueConsumer<M> consumer) {
while (!stopped && !consumer.isStopped()) {
try {
List<M> msgs = consumer.poll(config.getPollInterval());
if (msgs.isEmpty()) {
continue;
}
processMsgs(msgs, consumer, config);
} catch (Exception e) {
if (!consumer.isStopped()) {
log.warn("Failed to process messages from queue", e);
try {
Thread.sleep(config.getPollInterval());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
}
}
}
if (consumer.isStopped()) {
consumer.unsubscribe();
}
}
protected void processMsgs(List<M> msgs, QueueConsumer<M> consumer, C config) throws Exception {
msgPackProcessor.process(msgs, consumer, config);
}
public void stop() {
log.debug("[{}] Stopping consumers", queueName);
consumerWrapper.getConsumers().forEach(QueueConsumerTask::initiateStop);
stopped = true;
}
public void awaitStop() {
log.debug("[{}] Waiting for consumers to stop", queueName);
consumerWrapper.getConsumers().forEach(QueueConsumerTask::awaitCompletion);
log.debug("[{}] Unsubscribed and stopped consumers", queueName);
}
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) {
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]"));
}
public interface MsgPackProcessor<M extends QueueMsg, C extends QueueConfig> {
void process(List<M> msgs, QueueConsumer<M> consumer, C config) throws Exception;
}
public interface ConsumerWrapper<M extends QueueMsg> {
void updatePartitions(Set<TopicPartitionInfo> partitions);
Collection<QueueConsumerTask<M>> getConsumers();
}
class ConsumerPerPartitionWrapper implements ConsumerWrapper<M> {
private final Map<TopicPartitionInfo, QueueConsumerTask<M>> consumers = new HashMap<>();
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions);
addedPartitions.removeAll(consumers.keySet());
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet());
removedPartitions.removeAll(partitions);
log.info("[{}] Added partitions: {}, removed partitions: {}", queueName, partitionsToString(addedPartitions), partitionsToString(removedPartitions));
removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop());
removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion());
addedPartitions.forEach((tpi) -> {
Integer partitionId = tpi.getPartition().orElse(-1);
String key = queueName + "-" + partitionId;
QueueConsumerTask<M> consumer = new QueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId));
consumers.put(tpi, consumer);
consumer.subscribe(Set.of(tpi));
launchConsumer(consumer);
});
}
@Override
public Collection<QueueConsumerTask<M>> getConsumers() {
return consumers.values();
}
}
class SingleConsumerWrapper implements ConsumerWrapper<M> {
private QueueConsumerTask<M> consumer;
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
log.info("[{}] New partitions: {}", queueName, partitionsToString(partitions));
if (partitions.isEmpty()) {
if (consumer != null && consumer.isRunning()) {
consumer.initiateStop();
consumer.awaitCompletion();
}
consumer = null;
return;
}
if (consumer == null) {
consumer = new QueueConsumerTask<>(queueName, () -> consumerCreator.apply(config, null)); // no partitionId passed
}
consumer.subscribe(partitions);
if (!consumer.isRunning()) {
launchConsumer(consumer);
}
}
@Override
public Collection<QueueConsumerTask<M>> getConsumers() {
if (consumer == null) {
return Collections.emptyList();
}
return List.of(consumer);
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import sanbing.jcpp.infrastructure.queue.common.QueueConfig;
import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo;
import java.util.Set;
@Getter
@ToString
@AllArgsConstructor
public class QueueConsumerManagerTask {
private final QueueEvent event;
private QueueConfig config;
private Set<TopicPartitionInfo> partitions;
private boolean drainQueue;
public static QueueConsumerManagerTask delete(boolean drainQueue) {
return new QueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue);
}
public static QueueConsumerManagerTask configUpdate(QueueConfig config) {
return new QueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false);
}
public static QueueConsumerManagerTask partitionChange(Set<TopicPartitionInfo> partitions) {
return new QueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false);
}
}

View File

@@ -0,0 +1,78 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.queue.QueueConsumer;
import sanbing.jcpp.infrastructure.queue.QueueMsg;
import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@Slf4j
public class QueueConsumerTask<M extends QueueMsg> {
@Getter
private final Object key;
private volatile QueueConsumer<M> consumer;
private volatile Supplier<QueueConsumer<M>> consumerSupplier;
@Setter
private Future<?> task;
public QueueConsumerTask(Object key, Supplier<QueueConsumer<M>> consumerSupplier) {
this.key = key;
this.consumer = null;
this.consumerSupplier = consumerSupplier;
}
public QueueConsumer<M> getConsumer() {
if (consumer == null) {
synchronized (this) {
if (consumer == null) {
Objects.requireNonNull(consumerSupplier, "consumerSupplier for key [" + key + "] is null");
consumer = consumerSupplier.get();
Objects.requireNonNull(consumer, "consumer for key [" + key + "] is null");
consumerSupplier = null;
}
}
}
return consumer;
}
public void subscribe(Set<TopicPartitionInfo> partitions) {
log.info("[{}] Subscribing to partitions: {}", key, partitions);
getConsumer().subscribe(partitions);
}
public void initiateStop() {
log.debug("[{}] Initiating stop", key);
getConsumer().stop();
}
public void awaitCompletion() {
log.trace("[{}] Awaiting finish", key);
if (isRunning()) {
try {
task.get(30, TimeUnit.SECONDS);
log.trace("[{}] Awaited finish", key);
} catch (Exception e) {
log.warn("[{}] Failed to await for consumer to stop", key, e);
}
task = null;
}
}
public boolean isRunning() {
return task != null;
}
}

View File

@@ -0,0 +1,13 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue;
import java.io.Serializable;
public enum QueueEvent implements Serializable {
PARTITION_CHANGE, CONFIG_UPDATE, DELETE
}

View File

@@ -0,0 +1,236 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.app.service.queue.consumer;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import sanbing.jcpp.app.service.PileProtocolService;
import sanbing.jcpp.app.service.queue.AbstractConsumerService;
import sanbing.jcpp.app.service.queue.AppConsumerStats;
import sanbing.jcpp.app.service.queue.AppQueueConsumerManager;
import sanbing.jcpp.infrastructure.queue.*;
import sanbing.jcpp.infrastructure.queue.common.QueueConfig;
import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.event.PartitionChangeEvent;
import sanbing.jcpp.infrastructure.queue.processing.IdMsgPair;
import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.annotation.AppComponent;
import sanbing.jcpp.infrastructure.util.codec.ByteUtil;
import sanbing.jcpp.infrastructure.util.mdc.MDCUtils;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.infrastructure.util.trace.TracerRunnable;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN;
/**
* @author baigod
*/
@Service
@AppComponent
@Slf4j
public class ProtocolUplinkConsumerService extends AbstractConsumerService implements ApplicationListener<PartitionChangeEvent> {
@Value("${queue.app.poll-interval}")
private int pollInterval;
@Value("${queue.app.pack-processing-timeout}")
private long packProcessingTimeout;
@Value("${queue.app.consumer-per-partition}")
private boolean consumerPerPartition;
@Value("${queue.app.stats.enabled}")
private boolean statsEnabled;
private final PileProtocolService pileProtocolService;
private final AppQueueFactory appQueueFactory;
private AppQueueConsumerManager<ProtoQueueMsg<UplinkQueueMessage>, AppQueueConfig> appConsumer;
private final AppConsumerStats stats;
public ProtocolUplinkConsumerService(PartitionProvider partitionProvider,
ApplicationEventPublisher eventPublisher,
PileProtocolService pileProtocolService,
AppQueueFactory appQueueFactory,
StatsFactory statsFactory) {
super(partitionProvider, eventPublisher);
this.pileProtocolService = pileProtocolService;
this.appQueueFactory = appQueueFactory;
this.stats = new AppConsumerStats(statsFactory);
}
@PostConstruct
public void init() {
super.init("jcpp-app");
log.info("Initializing Protocol Uplink Messages Queue Subscriptions.");
this.appConsumer = AppQueueConsumerManager.<ProtoQueueMsg<UplinkQueueMessage>, AppQueueConfig>builder()
.queueName("protocol uplink")
.config(AppQueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> appQueueFactory.createProtocolUplinkMsgConsumer())
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)
.build();
}
@PreDestroy
public void destroy() {
super.destroy();
}
@Override
protected void stopConsumers() {
super.stopConsumers();
appConsumer.stop();
appConsumer.awaitStop();
}
@Scheduled(fixedDelayString = "${queue.app.stats.print-interval-ms}")
public void printStats() {
if (statsEnabled) {
stats.printStats();
stats.reset();
}
}
private void processMsgs(List<ProtoQueueMsg<UplinkQueueMessage>> msgs, QueueConsumer<ProtoQueueMsg<UplinkQueueMessage>> consumer, AppQueueConfig config) throws Exception {
List<IdMsgPair<UplinkQueueMessage>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, ProtoQueueMsg<UplinkQueueMessage>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
PackProcessingContext<ProtoQueueMsg<UplinkQueueMessage>> ctx = new PackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
Future<?> packSubmitFuture = consumersExecutor.submit(new TracerRunnable(() ->
orderedMsgList.forEach((element) -> {
UUID id = element.getUuid();
ProtoQueueMsg<UplinkQueueMessage> msg = element.getMsg();
tracer(msg);
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
Callback callback = new PackCallback<>(id, ctx);
try {
UplinkQueueMessage uplinkQueueMsg = msg.getValue();
pendingMsgHolder.setUplinkQueueMessage(uplinkQueueMsg);
if (uplinkQueueMsg.hasLoginRequest()) {
pileProtocolService.pileLogin(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasHeartBeatRequest()) {
pileProtocolService.heartBeat(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasVerifyPricingRequest()) {
pileProtocolService.verifyPricing(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasQueryPricingRequest()) {
pileProtocolService.queryPricing(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasGunRunStatusProto()) {
pileProtocolService.postGunRunStatus(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasChargingProgressProto()) {
pileProtocolService.postChargingProgress(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasSetPricingResponse()) {
pileProtocolService.onSetPricingResponse(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasRemoteStartChargingResponse()) {
pileProtocolService.onRemoteStartChargingResponse(uplinkQueueMsg, callback);
} else if (uplinkQueueMsg.hasRemoteStopChargingResponse()) {
pileProtocolService.onRemoteStopChargingResponse(uplinkQueueMsg, callback);
} else if(uplinkQueueMsg.hasTransactionRecord()){
pileProtocolService.onTransactionRecord(uplinkQueueMsg, callback);
}else {
callback.onSuccess();
}
if (statsEnabled) {
stats.log(uplinkQueueMsg);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
}
}))
);
if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
if (!packSubmitFuture.isDone()) {
packSubmitFuture.cancel(true);
UplinkQueueMessage lastSubmitMsg = pendingMsgHolder.getUplinkQueueMessage();
log.warn("Timeout to process message: {}", lastSubmitMsg);
}
if (log.isDebugEnabled()) {
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
}
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
consumer.commit();
}
private void tracer(ProtoQueueMsg<UplinkQueueMessage> msg) {
Optional.ofNullable(msg.getHeaders().get(MSG_MD_PREFIX + JCPP_TRACER_ID))
.map(tracerId -> {
String origin = null;
byte[] tracerOrigin = msg.getHeaders().get(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN);
if (tracerOrigin != null) {
origin = ByteUtil.bytesToString(tracerOrigin);
}
long ts = System.currentTimeMillis();
byte[] tracerTs = msg.getHeaders().get(MSG_MD_PREFIX + MSG_MD_TS);
if (tracerTs != null) {
ts = ByteUtil.bytesToLong(tracerTs);
}
return TracerContextUtil.newTracer(ByteUtil.bytesToString(tracerId), origin, ts);
})
.orElseGet(TracerContextUtil::newTracer);
MDCUtils.recordTracer();
}
@Override
protected int getMgmtThreadPoolSize() {
return Math.max(Runtime.getRuntime().availableProcessors(), 4);
}
@Override
protected void onJCPPApplicationEvent(PartitionChangeEvent event) {
Set<TopicPartitionInfo> appPartitions = event.getAppPartitions();
log.info("Subscribing to partitions: {}", appPartitions);
appConsumer.update(appPartitions);
}
@Data(staticConstructor = "of")
public static class AppQueueConfig implements QueueConfig {
private final boolean consumerPerPartition;
private final int pollInterval;
}
@Setter
@Getter
private static class PendingMsgHolder {
private volatile UplinkQueueMessage uplinkQueueMessage;
}
}