Merge branch 'dev' into feature-business-minigram

This commit is contained in:
Lemon
2026-01-19 13:28:10 +08:00
73 changed files with 10776 additions and 14 deletions

View File

@@ -0,0 +1,93 @@
package com.jsowell.pile.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jsowell.common.annotation.Excel;
import com.jsowell.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* JCPP 充电桩同步记录对象 jcpp_sync_record
*
* @author jsowell
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class JcppSyncRecord extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 同步类型FULL-全量INCREMENTAL-增量)
*/
@Excel(name = "同步类型")
private String syncType;
/**
* 同步状态RUNNING-进行中SUCCESS-成功FAILED-失败)
*/
@Excel(name = "同步状态")
private String syncStatus;
/**
* 开始时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/**
* 结束时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/**
* 总充电桩数
*/
@Excel(name = "总充电桩数")
private Integer totalPiles;
/**
* 成功充电桩数
*/
@Excel(name = "成功充电桩数")
private Integer successPiles;
/**
* 失败充电桩数
*/
@Excel(name = "失败充电桩数")
private Integer failedPiles;
/**
* 总充电枪数
*/
@Excel(name = "总充电枪数")
private Integer totalGuns;
/**
* 成功充电枪数
*/
@Excel(name = "成功充电枪数")
private Integer successGuns;
/**
* 失败充电枪数
*/
@Excel(name = "失败充电枪数")
private Integer failedGuns;
/**
* 错误信息
*/
private String errorMessage;
}

View File

@@ -0,0 +1,115 @@
package com.jsowell.pile.jcpp.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
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.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* JCPP 配置类
*
* @author jsowell
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "jcpp")
public class JcppConfig {
/**
* JCPP 服务地址
*/
private String url = "http://localhost:8180";
/**
* 下行接口路径
*/
private String downlinkPath = "/api/downlink";
/**
* 会话查询接口路径
*/
private String sessionPath = "/api/session";
/**
* 请求超时时间(毫秒)
*/
private int timeout = 5000;
/**
* 连接超时时间(毫秒)
*/
private int connectTimeout = 3000;
/**
* 同步接口超时时间(毫秒)- 批量同步需要更长时间
*/
private int syncTimeout = 120000;
/**
* 同步接口连接超时时间(毫秒)
*/
private int syncConnectTimeout = 10000;
/**
* 是否启用 JCPP 对接
*/
private boolean enabled = true;
/**
* 重试次数
*/
private int retryCount = 3;
/**
* 重试间隔(毫秒)
*/
private int retryInterval = 1000;
/**
* 获取下行接口完整 URL
*/
public String getDownlinkUrl() {
return url + downlinkPath;
}
/**
* 获取会话查询接口完整 URL
*/
public String getSessionUrl() {
return url + sessionPath;
}
/**
* 创建 JCPP 专用的 RestTemplate默认
*/
@Primary
@Bean("jcppRestTemplate")
public RestTemplate jcppRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(connectTimeout);
factory.setReadTimeout(timeout);
RestTemplate restTemplate = new RestTemplate(factory);
log.info("JCPP RestTemplate 初始化完成, url: {}, timeout: {}ms, connectTimeout: {}ms",
url, timeout, connectTimeout);
return restTemplate;
}
/**
* 创建 JCPP 同步专用的 RestTemplate超时时间更长
*/
@Bean("jcppSyncRestTemplate")
public RestTemplate jcppSyncRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(syncConnectTimeout);
factory.setReadTimeout(syncTimeout);
RestTemplate restTemplate = new RestTemplate(factory);
log.info("JCPP 同步 RestTemplate 初始化完成, syncTimeout: {}ms, syncConnectTimeout: {}ms",
syncTimeout, syncConnectTimeout);
return restTemplate;
}
}

View File

@@ -0,0 +1,82 @@
package com.jsowell.pile.jcpp.config;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.util.JcppPartitionCalculator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* JCPP Protobuf 消息分区队列配置
* 实现基于 messageKey 的分区消费,确保同一充电桩的消息顺序处理
*
* @author jsowell
*/
@Slf4j
@Configuration
public class JcppPartitionQueueConfig {
@Value("${jcpp.rabbitmq.partition-count:10}")
private int partitionCount;
@PostConstruct
public void init() {
// 设置分区数量到计算器
JcppPartitionCalculator.setPartitionCount(partitionCount);
log.info("JCPP 分区队列配置初始化完成,分区数量: {}", partitionCount);
}
/**
* 创建分区队列数组
* 每个分区一个队列,用于顺序消费
*/
@Bean
public Queue[] jcppPartitionQueues() {
Queue[] queues = new Queue[partitionCount];
for (int i = 0; i < partitionCount; i++) {
String queueName = JcppPartitionCalculator.getQueueName(i);
queues[i] = new Queue(queueName, true, false, false);
log.info("创建 JCPP 分区队列: {}", queueName);
}
return queues;
}
/**
* 绑定分区队列到 Exchange
* 每个分区队列绑定所有消息类型jcpp.uplink.#
* 实际分区由 JCPP 在发送消息时通过 header 指定
*
* 注意:复用 JcppRabbitConfig 中定义的 jcppUplinkExchange Bean
*/
@Bean
public Binding[] jcppPartitionBindings(
TopicExchange jcppUplinkExchange,
Queue[] jcppPartitionQueues) {
List<Binding> bindings = new ArrayList<>();
for (int i = 0; i < partitionCount; i++) {
// 每个分区队列绑定所有消息类型
Binding binding = BindingBuilder
.bind(jcppPartitionQueues[i])
.to(jcppUplinkExchange)
.with("jcpp.uplink.#");
bindings.add(binding);
log.info("绑定分区队列 {} 到 Exchange: {}",
jcppPartitionQueues[i].getName(),
jcppUplinkExchange.getName());
}
return bindings.toArray(new Binding[0]);
}
}

View File

@@ -0,0 +1,176 @@
package com.jsowell.pile.jcpp.config;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* JCPP RabbitMQ 配置类
*
* @author jsowell
*/
@Configuration
public class JcppRabbitConfig {
// ==================== Exchange ====================
/**
* 上行消息 Exchange
*/
@Bean
public TopicExchange jcppUplinkExchange() {
return new TopicExchange(JcppConstants.UPLINK_EXCHANGE, true, false);
}
// ==================== Queues ====================
/**
* 登录消息队列
*/
@Bean
public Queue jcppLoginQueue() {
return new Queue(JcppConstants.QUEUE_LOGIN, true, false, false);
}
/**
* 心跳消息队列
*/
@Bean
public Queue jcppHeartbeatQueue() {
return new Queue(JcppConstants.QUEUE_HEARTBEAT, true, false, false);
}
/**
* 启动充电消息队列
*/
@Bean
public Queue jcppStartChargeQueue() {
return new Queue(JcppConstants.QUEUE_START_CHARGE, true, false, false);
}
/**
* 实时数据消息队列
*/
@Bean
public Queue jcppRealTimeDataQueue() {
return new Queue(JcppConstants.QUEUE_REAL_TIME_DATA, true, false, false);
}
/**
* 交易记录消息队列
*/
@Bean
public Queue jcppTransactionQueue() {
return new Queue(JcppConstants.QUEUE_TRANSACTION, true, false, false);
}
/**
* 枪状态消息队列
*/
@Bean
public Queue jcppGunStatusQueue() {
return new Queue(JcppConstants.QUEUE_GUN_STATUS, true, false, false);
}
/**
* 计费模板消息队列
*/
@Bean
public Queue jcppPricingQueue() {
return new Queue(JcppConstants.QUEUE_PRICING, true, false, false);
}
/**
* 远程操作结果消息队列
*/
@Bean
public Queue jcppRemoteResultQueue() {
return new Queue(JcppConstants.QUEUE_REMOTE_RESULT, true, false, false);
}
/**
* 会话关闭消息队列
*/
@Bean
public Queue jcppSessionCloseQueue() {
return new Queue(JcppConstants.QUEUE_SESSION_CLOSE, true, false, false);
}
// ==================== Bindings ====================
/**
* 登录消息绑定
*/
@Bean
public Binding jcppLoginBinding(Queue jcppLoginQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppLoginQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_LOGIN);
}
/**
* 心跳消息绑定
*/
@Bean
public Binding jcppHeartbeatBinding(Queue jcppHeartbeatQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppHeartbeatQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_HEARTBEAT);
}
/**
* 启动充电消息绑定
*/
@Bean
public Binding jcppStartChargeBinding(Queue jcppStartChargeQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppStartChargeQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_START_CHARGE);
}
/**
* 实时数据消息绑定
*/
@Bean
public Binding jcppRealTimeDataBinding(Queue jcppRealTimeDataQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppRealTimeDataQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_REAL_TIME_DATA);
}
/**
* 交易记录消息绑定
*/
@Bean
public Binding jcppTransactionBinding(Queue jcppTransactionQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppTransactionQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_TRANSACTION);
}
/**
* 枪状态消息绑定
*/
@Bean
public Binding jcppGunStatusBinding(Queue jcppGunStatusQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppGunStatusQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_GUN_STATUS);
}
/**
* 计费模板消息绑定(通配符)
*/
@Bean
public Binding jcppPricingBinding(Queue jcppPricingQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppPricingQueue).to(jcppUplinkExchange).with("jcpp.uplink.pricing.#");
}
/**
* 远程操作结果消息绑定(通配符)
*/
@Bean
public Binding jcppRemoteResultBinding(Queue jcppRemoteResultQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppRemoteResultQueue).to(jcppUplinkExchange).with("jcpp.uplink.remoteResult.#");
}
/**
* 会话关闭消息绑定
*/
@Bean
public Binding jcppSessionCloseBinding(Queue jcppSessionCloseQueue, TopicExchange jcppUplinkExchange) {
return BindingBuilder.bind(jcppSessionCloseQueue).to(jcppUplinkExchange).with(JcppConstants.ROUTING_KEY_SESSION_CLOSE);
}
}

View File

@@ -0,0 +1,395 @@
package com.jsowell.pile.jcpp.constant;
/**
* JCPP 常量类
*
* @author jsowell
*/
public class JcppConstants {
private JcppConstants() {
}
// ==================== RabbitMQ Exchange ====================
/**
* 上行消息 Exchange
*/
public static final String UPLINK_EXCHANGE = "jcpp.uplink.exchange";
// ==================== RabbitMQ Queues ====================
/**
* 登录消息队列
*/
public static final String QUEUE_LOGIN = "jcpp.login.queue";
/**
* 心跳消息队列
*/
public static final String QUEUE_HEARTBEAT = "jcpp.heartbeat.queue";
/**
* 启动充电消息队列
*/
public static final String QUEUE_START_CHARGE = "jcpp.startCharge.queue";
/**
* 实时数据消息队列
*/
public static final String QUEUE_REAL_TIME_DATA = "jcpp.realTimeData.queue";
/**
* 交易记录消息队列
*/
public static final String QUEUE_TRANSACTION = "jcpp.transaction.queue";
/**
* 枪状态消息队列
*/
public static final String QUEUE_GUN_STATUS = "jcpp.gunStatus.queue";
/**
* 计费模板消息队列
*/
public static final String QUEUE_PRICING = "jcpp.pricing.queue";
/**
* 远程操作结果消息队列
*/
public static final String QUEUE_REMOTE_RESULT = "jcpp.remoteResult.queue";
/**
* 会话关闭消息队列
*/
public static final String QUEUE_SESSION_CLOSE = "jcpp.sessionClose.queue";
// ==================== RabbitMQ Routing Keys ====================
/**
* 登录消息路由键
*/
public static final String ROUTING_KEY_LOGIN = "jcpp.uplink.login";
/**
* 心跳消息路由键
*/
public static final String ROUTING_KEY_HEARTBEAT = "jcpp.uplink.heartbeat";
/**
* 启动充电消息路由键
*/
public static final String ROUTING_KEY_START_CHARGE = "jcpp.uplink.startCharge";
/**
* 实时数据消息路由键
*/
public static final String ROUTING_KEY_REAL_TIME_DATA = "jcpp.uplink.realTimeData";
/**
* 交易记录消息路由键
*/
public static final String ROUTING_KEY_TRANSACTION = "jcpp.uplink.transaction";
/**
* 枪状态消息路由键
*/
public static final String ROUTING_KEY_GUN_STATUS = "jcpp.uplink.gunStatus";
/**
* 会话关闭消息路由键
*/
public static final String ROUTING_KEY_SESSION_CLOSE = "jcpp.uplink.sessionClose";
// ==================== Redis Key 前缀 ====================
/**
* JCPP 下行指令 Redis Key 前缀
* 完整 key: jcpp:downlink:{pileCode}
*/
public static final String REDIS_DOWNLINK_PREFIX = "jcpp:downlink:";
/**
* JCPP 会话信息 Redis Key 前缀
* 完整 key: jcpp:session:{pileCode}
*/
public static final String REDIS_SESSION_PREFIX = "jcpp:session:";
/**
* 在线充电桩集合 Redis Key
*/
public static final String REDIS_ONLINE_PILES_KEY = "jcpp:online:piles";
/**
* JCPP 会话信息 Redis Key 前缀(兼容旧版本)
* 完整 key: jcpp:session:{pileCode}
*/
public static final String REDIS_KEY_SESSION = "jcpp:session:";
/**
* JCPP 节点信息 Redis Key 前缀
* 完整 key: jcpp:node:{pileCode}
*/
public static final String REDIS_KEY_NODE = "jcpp:node:";
/**
* 充电桩在线状态 Redis Key 前缀
* 完整 key: jcpp:online:{pileCode}
*/
public static final String REDIS_KEY_ONLINE = "jcpp:online:";
/**
* 会话过期时间(秒)- 默认5分钟
*/
public static final long SESSION_EXPIRE_SECONDS = 300L;
/**
* 在线状态过期时间(秒)- 默认3分钟
*/
public static final long ONLINE_EXPIRE_SECONDS = 180L;
// ==================== 消息类型枚举 ====================
/**
* 上行消息类型
*/
public static class MessageType {
/**
* 充电桩登录
*/
public static final String LOGIN = "LOGIN";
/**
* 心跳
*/
public static final String HEARTBEAT = "HEARTBEAT";
/**
* 刷卡/扫码启动充电
*/
public static final String START_CHARGE = "START_CHARGE";
/**
* 实时数据上报
*/
public static final String REAL_TIME_DATA = "REAL_TIME_DATA";
/**
* 交易记录(充电结束)
*/
public static final String TRANSACTION_RECORD = "TRANSACTION_RECORD";
/**
* 枪状态变化
*/
public static final String GUN_STATUS = "GUN_STATUS";
/**
* 校验计费模板
*/
public static final String VERIFY_PRICING = "VERIFY_PRICING";
/**
* 查询计费模板
*/
public static final String QUERY_PRICING = "QUERY_PRICING";
/**
* 远程启动结果
*/
public static final String REMOTE_START_RESULT = "REMOTE_START_RESULT";
/**
* 远程停止结果
*/
public static final String REMOTE_STOP_RESULT = "REMOTE_STOP_RESULT";
/**
* 会话关闭
*/
public static final String SESSION_CLOSE = "SESSION_CLOSE";
private MessageType() {
}
}
// ==================== 下行指令类型 ====================
/**
* 下行指令类型
*/
public static class DownlinkCommand {
/**
* 登录应答
*/
public static final String LOGIN_ACK = "LOGIN_ACK";
/**
* 远程启动充电
*/
public static final String REMOTE_START = "REMOTE_START";
/**
* 远程停止充电
*/
public static final String REMOTE_STOP = "REMOTE_STOP";
/**
* 下发计费模板
*/
public static final String SET_PRICING = "SET_PRICING";
/**
* 查询计费应答
*/
public static final String QUERY_PRICING_ACK = "QUERY_PRICING_ACK";
/**
* 校验计费应答
*/
public static final String VERIFY_PRICING_ACK = "VERIFY_PRICING_ACK";
/**
* 启动充电应答
*/
public static final String START_CHARGE_ACK = "START_CHARGE_ACK";
/**
* 交易记录应答
*/
public static final String TRANSACTION_RECORD_ACK = "TRANSACTION_RECORD_ACK";
private DownlinkCommand() {
}
}
// ==================== 启动类型 ====================
/**
* 充电启动类型
*/
public static class StartType {
/**
* 刷卡启动
*/
public static final String CARD = "CARD";
/**
* APP/小程序启动
*/
public static final String APP = "APP";
/**
* VIN码启动
*/
public static final String VIN = "VIN";
private StartType() {
}
}
// ==================== 鉴权失败原因 ====================
/**
* 鉴权失败原因
*/
public static class AuthFailReason {
/**
* 账户不存在
*/
public static final String ACCOUNT_NOT_EXISTS = "ACCOUNT_NOT_EXISTS";
/**
* 账户冻结
*/
public static final String ACCOUNT_FROZEN = "ACCOUNT_FROZEN";
/**
* 余额不足
*/
public static final String INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE";
/**
* 密码错误
*/
public static final String PASSWORD_ERROR = "PASSWORD_ERROR";
/**
* 充电桩停用
*/
public static final String PILE_DISABLED = "PILE_DISABLED";
/**
* 充电枪故障
*/
public static final String GUN_FAULT = "GUN_FAULT";
/**
* 充电枪占用
*/
public static final String GUN_OCCUPIED = "GUN_OCCUPIED";
/**
* 系统错误
*/
public static final String SYSTEM_ERROR = "SYSTEM_ERROR";
private AuthFailReason() {
}
}
// ==================== 停止原因 ====================
/**
* 充电停止原因
*/
public static class StopReason {
/**
* 用户主动停止
*/
public static final String USER_STOP = "USER_STOP";
/**
* 充满自停
*/
public static final String FULL_STOP = "FULL_STOP";
/**
* 金额用尽
*/
public static final String BALANCE_EXHAUSTED = "BALANCE_EXHAUSTED";
/**
* 异常停止
*/
public static final String ABNORMAL_STOP = "ABNORMAL_STOP";
/**
* 远程停止
*/
public static final String REMOTE_STOP = "REMOTE_STOP";
private StopReason() {
}
}
// ==================== 计费类型 ====================
/**
* 计费明细类型
*/
public static class PricingDetailType {
/**
* 峰谷计费
*/
public static final String PEAK_VALLEY = "PEAK_VALLEY";
/**
* 时段计费
*/
public static final String TIME_PERIOD = "TIME_PERIOD";
private PricingDetailType() {
}
}
}

View File

@@ -0,0 +1,106 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 枪状态消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppGunStatusConsumer {
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
/**
* 枪状态映射JCPP 状态 -> 系统状态
* JCPP: IDLE, INSERTED, CHARGING, CHARGE_COMPLETE, FAULT, UNKNOWN
* 系统: 0-离网, 1-空闲, 2-占用(未充电), 3-占用(充电中), 4-占用(预约锁定), 255-故障
*/
private String mapGunStatus(String jcppStatus) {
if (jcppStatus == null) {
return "0";
}
switch (jcppStatus) {
case "IDLE":
return "1"; // 空闲
case "INSERTED":
return "2"; // 占用(未充电)
case "CHARGING":
return "3"; // 占用(充电中)
case "CHARGE_COMPLETE":
return "2"; // 占用(未充电)- 充电完成但未拔枪
case "FAULT":
return "255"; // 故障
case "UNKNOWN":
default:
return "0"; // 离网
}
}
@RabbitListener(queues = JcppConstants.QUEUE_GUN_STATUS)
public void handleGunStatus(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("枪状态消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("枪状态消息缺少 pileCode");
return;
}
log.info("收到 JCPP 枪状态消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
// 从 data 中获取其他信息
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String gunRunStatus = data.getString("gunRunStatus");
JSONArray faultMessages = data.getJSONArray("faultMessages");
if (gunNo == null) {
log.warn("枪状态消息缺少 gunNo");
return;
}
// 构建枪口编码pileCode + gunNo
String pileConnectorCode = pileCode + gunNo;
// 映射状态
String status = mapGunStatus(gunRunStatus);
// 更新枪状态
int result = pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, status);
if (result > 0) {
log.info("更新枪状态成功: pileConnectorCode={}, status={}", pileConnectorCode, status);
} else {
log.warn("更新枪状态失败: pileConnectorCode={}, status={}", pileConnectorCode, status);
}
// 记录故障信息
if (faultMessages != null && !faultMessages.isEmpty()) {
log.warn("充电枪故障: pileConnectorCode={}, faults={}", pileConnectorCode, faultMessages);
// TODO: 可以将故障信息保存到数据库或发送告警
}
} catch (Exception e) {
log.error("处理 JCPP 枪状态消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,58 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* JCPP 心跳消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppHeartbeatConsumer {
private static final String HEARTBEAT_KEY_PREFIX = "jcpp:heartbeat:";
private static final long HEARTBEAT_EXPIRE_SECONDS = 180L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = JcppConstants.QUEUE_HEARTBEAT)
public void handleHeartbeat(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.debug("心跳消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.debug("心跳消息缺少 pileCode");
return;
}
// 更新最后活跃时间到 Redis避免频繁写数据库
String key = HEARTBEAT_KEY_PREFIX + pileCode;
stringRedisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),
HEARTBEAT_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.debug("收到充电桩心跳: pileCode={}", pileCode);
} catch (Exception e) {
log.error("处理 JCPP 心跳消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,90 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppJsonMessageHandler;
import com.jsowell.pile.jcpp.util.JcppPartitionCalculator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
/**
* JCPP JSON 消息消费者 - 分区消费
* 每个分区单线程消费,保证同一充电桩的消息顺序处理
*
* 注意:此消费者默认禁用,需要在 JCPP 端配置好分区路由并创建队列后再启用
* 启用方法:取消 @Component 注解的注释
*
* @author jsowell
*/
@Slf4j
// @Component // 暂时禁用,等 JCPP 端配置好分区路由并创建队列后再启用
public class JcppJsonPartitionConsumer {
@Autowired
private IJcppJsonMessageHandler messageHandler;
@RabbitListener(
queues = {
"jcpp.uplink.partition.0",
"jcpp.uplink.partition.1",
"jcpp.uplink.partition.2",
"jcpp.uplink.partition.3",
"jcpp.uplink.partition.4",
"jcpp.uplink.partition.5",
"jcpp.uplink.partition.6",
"jcpp.uplink.partition.7",
"jcpp.uplink.partition.8",
"jcpp.uplink.partition.9"
},
concurrency = "1" // 每个队列单线程消费保证顺序
)
public void consumeMessage(JcppUplinkMessage uplinkMessage,
@Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey,
@Header(AmqpHeaders.CONSUMER_QUEUE) String queueName) {
// 从队列名称提取分区编号
int partition = extractPartitionFromQueue(queueName);
try {
String messageKey = uplinkMessage.getPileCode();
String messageType = uplinkMessage.getMessageType();
log.info("[分区{}] 收到消息: pileCode={}, messageType={}, routingKey={}",
partition, messageKey, messageType, routingKey);
// 验证分区是否正确
int expectedPartition = JcppPartitionCalculator.getPartition(messageKey);
if (expectedPartition != partition) {
log.warn("[分区{}] 消息分区不匹配: pileCode={}, 期望分区={}, 实际分区={}",
partition, messageKey, expectedPartition, partition);
}
// 处理消息
messageHandler.handleUplinkMessage(uplinkMessage);
log.debug("[分区{}] 消息处理完成: pileCode={}", partition, messageKey);
} catch (Exception e) {
log.error("[分区{}] 消息处理失败: pileCode={}",
partition, uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
throw new RuntimeException("消息处理失败", e);
}
}
/**
* 从队列名称提取分区编号
* 队列名称格式: jcpp.uplink.partition.{partition}
*/
private int extractPartitionFromQueue(String queueName) {
try {
String[] parts = queueName.split("\\.");
return Integer.parseInt(parts[parts.length - 1]);
} catch (Exception e) {
log.error("无法从队列名称提取分区编号: {}", queueName, e);
return 0;
}
}
}

View File

@@ -0,0 +1,74 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.PileBasicInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 登录消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppLoginConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@RabbitListener(queues = JcppConstants.QUEUE_LOGIN)
public void handleLogin(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("登录消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("登录消息缺少 pileCode");
return;
}
log.info("收到 JCPP 登录消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
// 解析 data
JSONObject data = JSON.parseObject(uplinkMessage.getData());
// 查询充电桩是否存在
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
boolean exists = pileInfo != null;
if (exists) {
// 更新充电桩在线状态
// PileBasicInfo updateInfo = new PileBasicInfo();
// updateInfo.setId(pileInfo.getId());
// updateInfo.setOnlineStatus("1"); // 1-在线
// pileBasicInfoService.updatePileBasicInfo(updateInfo);
log.info("充电桩登录成功: pileCode={}", pileCode);
} else {
log.warn("充电桩不存在: pileCode={}", pileCode);
}
// 发送登录应答
jcppDownlinkService.sendLoginAck(pileCode, exists);
} catch (Exception e) {
log.error("处理 JCPP 登录消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,150 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.PileBillingTemplate;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.util.PricingModelConverter;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileBillingTemplateService;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 计费查询消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppPricingConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@RabbitListener(queues = JcppConstants.QUEUE_PRICING)
public void handlePricing(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("计费消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode 和 messageType
String pileCode = uplinkMessage.getPileCode();
String messageType = uplinkMessage.getMessageType();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("计费消息缺少 pileCode");
return;
}
log.info("收到 JCPP 计费消息: pileCode={}, messageType={}", pileCode, messageType);
// 解析 data
JSONObject data = JSON.parseObject(uplinkMessage.getData());
// 根据消息类型处理
if (JcppConstants.MessageType.QUERY_PRICING.equals(messageType)) {
handleQueryPricing(pileCode);
} else if (JcppConstants.MessageType.VERIFY_PRICING.equals(messageType)) {
Long pricingId = data.getLong("pricingId");
handleVerifyPricing(pileCode, pricingId);
} else {
log.warn("未知的计费消息类型: {}", messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 计费消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
/**
* 处理查询计费模板请求
*/
private void handleQueryPricing(String pileCode) {
try {
// 根据 pileCode 查询充电桩
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
log.warn("充电桩不存在: pileCode={}", pileCode);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
// 2. 获取关联的计费模板 ID
BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
if (billingTemplateVO == null || billingTemplateVO.getTemplateId() == null) {
log.warn("充电桩未配置计费模板: pileCode={}", pileCode);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
Long billingTemplateId = Long.parseLong(billingTemplateVO.getTemplateId());
// 获取充电桩关联的计费模板 ID
// Long billingTemplateId = pileInfo.getBillingTemplateId();
// if (billingTemplateId == null) {
// log.warn("充电桩未配置计费模板: pileCode={}", pileCode);
// jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
// return;
// }
// 查询计费模板
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(billingTemplateId);
if (template == null) {
log.warn("计费模板不存在: pileCode={}, billingTemplateId={}", pileCode, billingTemplateId);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
// 转换为 JCPP 格式
JcppPricingModel pricingModel = PricingModelConverter.convert(template);
// 发送应答
jcppDownlinkService.sendQueryPricingAck(pileCode, billingTemplateId, pricingModel);
log.info("发送计费模板查询应答: pileCode={}, pricingId={}", pileCode, billingTemplateId);
} catch (Exception e) {
log.error("处理查询计费模板异常: pileCode={}", pileCode, e);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
}
}
/**
* 处理校验计费模板请求
*/
private void handleVerifyPricing(String pileCode, Long pricingId) {
try {
boolean success = false;
if (pricingId != null) {
// 查询计费模板是否存在
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(pricingId);
success = template != null;
}
// 发送应答
jcppDownlinkService.sendVerifyPricingAck(pileCode, success, pricingId);
log.info("发送计费模板校验应答: pileCode={}, pricingId={}, success={}", pileCode, pricingId, success);
} catch (Exception e) {
log.error("处理校验计费模板异常: pileCode={}, pricingId={}", pileCode, pricingId, e);
jcppDownlinkService.sendVerifyPricingAck(pileCode, false, pricingId);
}
}
}

View File

@@ -0,0 +1,113 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
/**
* JCPP 实时数据消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppRealTimeDataConsumer {
private static final String REALTIME_DATA_KEY_PREFIX = "jcpp:realtime:";
private static final long REALTIME_DATA_EXPIRE_SECONDS = 300L;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = JcppConstants.QUEUE_REAL_TIME_DATA)
public void handleRealTimeData(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.debug("实时数据消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.debug("实时数据消息缺少 pileCode");
return;
}
// 从 data 中获取其他信息
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
String outputVoltage = data.getString("outputVoltage");
String outputCurrent = data.getString("outputCurrent");
Integer soc = data.getInteger("soc");
Integer totalChargingDurationMin = data.getInteger("totalChargingDurationMin");
String totalChargingEnergyKWh = data.getString("totalChargingEnergyKWh");
String totalChargingCostYuan = data.getString("totalChargingCostYuan");
if (tradeNo == null || tradeNo.isEmpty()) {
log.debug("实时数据消息缺少 tradeNo");
return;
}
// 将实时数据缓存到 Redis避免频繁写数据库
String key = REALTIME_DATA_KEY_PREFIX + tradeNo;
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(data),
REALTIME_DATA_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.debug("订单不存在: tradeNo={}", tradeNo);
return;
}
// 检查订单状态是否为充电中
if (order.getOrderStatus() != null && StringUtils.equals(order.getOrderStatus(), "1")) {
// 更新订单实时数据
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
if (totalChargingEnergyKWh != null) {
// updateOrder.setTotalElectricity(new BigDecimal(totalChargingEnergyKWh));
}
if (totalChargingCostYuan != null) {
updateOrder.setOrderAmount(new BigDecimal(totalChargingCostYuan));
}
if (totalChargingDurationMin != null) {
// updateOrder.setChargingDuration(totalChargingDurationMin);
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
}
// 更新枪状态为充电中
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
log.debug("处理实时数据: tradeNo={}, soc={}, energy={}kWh", tradeNo, soc, totalChargingEnergyKWh);
} catch (Exception e) {
log.error("处理 JCPP 实时数据消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,131 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* JCPP 远程操作结果消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppRemoteResultConsumer {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_REMOTE_RESULT)
public void handleRemoteResult(JcppUplinkMessage uplinkMessage) {
try {
log.info("收到 JCPP 远程操作结果消息: pileCode={}, messageType={}",
uplinkMessage.getPileCode(), uplinkMessage.getMessageType());
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("远程操作结果消息格式错误");
return;
}
String messageType = uplinkMessage.getMessageType();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
// 根据消息类型处理
if (JcppConstants.MessageType.REMOTE_START_RESULT.equals(messageType)) {
handleRemoteStartResult(data);
} else if (JcppConstants.MessageType.REMOTE_STOP_RESULT.equals(messageType)) {
handleRemoteStopResult(data);
} else {
log.warn("未知的远程操作结果消息类型: {}", messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 远程操作结果消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
/**
* 处理远程启动结果
*/
private void handleRemoteStartResult(JSONObject data) {
String tradeNo = data.getString("tradeNo");
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
// 从 data 中获取 pileCode 和 gunNo如果有
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("远程启动结果缺少 tradeNo");
return;
}
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在: tradeNo={}", tradeNo);
return;
}
if (Boolean.TRUE.equals(success)) {
// 启动成功:更新订单状态为充电中
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("1"); // 充电中
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
// 更新枪状态为充电中
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
log.info("远程启动成功: tradeNo={}", tradeNo);
} else {
// 启动失败:更新订单状态为启动失败
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("3"); // 已取消/启动失败
updateOrder.setReason("启动失败: " + failReason);
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
log.warn("远程启动失败: tradeNo={}, reason={}", tradeNo, failReason);
// TODO: 如果已预付费,触发退款流程
}
}
/**
* 处理远程停止结果
*/
private void handleRemoteStopResult(JSONObject data) {
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
// 从 data 中获取 pileCode 和 gunNo如果有
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
if (Boolean.TRUE.equals(success)) {
log.info("远程停止成功: pileCode={}, gunNo={}", pileCode, gunNo);
} else {
log.warn("远程停止失败: pileCode={}, gunNo={}, reason=", pileCode, gunNo, failReason);
}
// 等待交易记录消息进行最终结算
}
}

View File

@@ -0,0 +1,77 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JCPP 会话关闭消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppSessionCloseConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@RabbitListener(queues = JcppConstants.QUEUE_SESSION_CLOSE)
public void handleSessionClose(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("会话关闭消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("会话关闭消息缺少 pileCode");
return;
}
log.info("收到 JCPP 会话关闭消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
// 从 data 中获取其他信息
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String reason = data.getString("reason");
log.info("充电桩会话关闭: pileCode={}, reason={}", pileCode, reason);
// 1. 更新充电桩离线状态
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo != null) {
PileBasicInfo updateInfo = new PileBasicInfo();
updateInfo.setId(pileInfo.getId());
// updateInfo.setOnlineStatus(0); // 0-离线
pileBasicInfoService.updatePileBasicInfo(updateInfo);
log.info("更新充电桩离线状态: pileCode={}", pileCode);
}
// 2. 更新所有枪状态为离线
int result = pileConnectorInfoService.updateConnectorStatusByPileSn(pileCode, "0");
log.info("更新枪状态为离线: pileCode={}, affectedRows={}", pileCode, result);
// 3. TODO: 查询是否有正在充电的订单,如果有则标记为异常
// 这里需要根据实际业务逻辑处理正在充电的订单
// 可以调用 OrderBasicInfoService 查询并处理
} catch (Exception e) {
log.error("处理 JCPP 会话关闭消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,253 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.enums.ykc.StartTypeEnum;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.id.IdUtils;
import com.jsowell.pile.domain.*;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
/**
* JCPP 刷卡启动充电消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppStartChargeConsumer {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private PileAuthCardService pileAuthCardService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private PileStationWhitelistService pileStationWhitelistService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_START_CHARGE)
public void handleStartCharge(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("启动充电消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("启动充电消息缺少 pileCode");
return;
}
log.info("收到 JCPP 启动充电消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
// 从 data 中获取其他信息
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String startType = data.getString("startType");
String cardNo = data.getString("cardNo");
Boolean needPassword = data.getBoolean("needPassword");
String password = data.getString("password");
if (gunNo == null) {
log.warn("启动充电消息缺少 gunNo");
return;
}
// 处理刷卡启动
if ("CARD".equals(startType)) {
handleCardStartCharge(pileCode, gunNo, cardNo, needPassword, password);
} else {
log.warn("不支持的启动类型: {}", startType);
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, null, cardNo, null,
false, "不支持的启动类型");
}
} catch (Exception e) {
log.error("处理 JCPP 启动充电消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
/**
* 处理刷卡启动充电
*/
private void handleCardStartCharge(String pileCode, String gunNo, String cardNo,
Boolean needPassword, String password) {
String failReason = null;
String tradeNo = null;
String limitYuan = null;
boolean authSuccess = false;
try {
// 1. 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
failReason = "充电桩不存在";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查充电桩状态
if (pileInfo.getDelFlag() != null && StringUtils.equals(pileInfo.getDelFlag(), "1")) {
failReason = "充电桩已停用";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 2. 查询授权卡信息
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
if (authCard == null) {
failReason = "账户不存在";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查卡状态
if (authCard.getStatus() != null && !StringUtils.equals(authCard.getStatus(), "1")) {
failReason = "账户已冻结";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 3. 如果需要密码验证
// if (Boolean.TRUE.equals(needPassword)) {
// if (password == null || !password.equals(authCard.getPassword())) {
// failReason = "密码错误";
// sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
// return;
// }
// }
// 4. 查询会员信息和钱包余额
String memberId = authCard.getMemberId();
BigDecimal balance = BigDecimal.ZERO;
if (memberId != null) {
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, String.valueOf(pileInfo.getMerchantId()));
if (walletInfo != null) {
balance = walletInfo.getPrincipalBalance();
if (walletInfo.getGiftBalance() != null) {
balance = balance.add(walletInfo.getGiftBalance());
}
}
}
// 5. 检查白名单
boolean isWhitelist = checkWhitelist(pileInfo.getStationId(), cardNo, memberId);
// 6. 验证余额(非白名单用户需要检查余额)
BigDecimal minAmount = new BigDecimal("1.00"); // 最低充电金额
if (!isWhitelist && balance.compareTo(minAmount) < 0) {
failReason = "余额不足";
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 7. 生成交易流水号
tradeNo = IdUtils.fastSimpleUUID();
limitYuan = balance.toString();
// 8. 创建充电订单
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + gunNo);
order.setMemberId(memberId);
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setOrderStatus("0"); // 待支付/启动中
order.setPayMode(String.valueOf(isWhitelist ? 3 : 1)); // 3-白名单支付, 1-余额支付
order.setCreateTime(new Date());
order.setStartType(StartTypeEnum.NOW.getValue()); // 刷卡启动
orderBasicInfoService.insert(order);
log.info("创建充电订单: tradeNo={}, pileCode={}, gunNo={}", tradeNo, pileCode, gunNo);
authSuccess = true;
} catch (Exception e) {
log.error("处理刷卡启动充电异常: pileCode={}, gunNo={}, cardNo={}", pileCode, gunNo, cardNo, e);
failReason = "系统错误";
}
// 发送鉴权结果
sendAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, authSuccess, failReason);
}
/**
* 检查白名单
*/
private boolean checkWhitelist(Long stationId, String cardNo, String memberId) {
try {
if (stationId == null) {
return false;
}
// 查询白名单
PileStationWhitelist pileStationWhitelist = pileStationWhitelistService.queryWhitelistByMemberId(String.valueOf(stationId), memberId);
if (pileStationWhitelist == null) {
return false;
}
return true;
// for (PileStationWhitelist whitelist : whitelists) {
// // 检查卡号
// if (cardNo != null && cardNo.equals(whitelist.getLogicCardNo())) {
// return true;
// }
// // 检查会员ID
// if (memberId != null && memberId.equals(whitelist.getMemberId())) {
// return true;
// }
// }
} catch (Exception e) {
log.error("检查白名单异常: stationId={}", stationId, e);
}
return false;
}
/**
* 发送启动充电应答
*/
private void sendAck(String pileCode, String gunNo, String tradeNo, String cardNo,
String limitYuan, boolean authSuccess, String failReason) {
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan,
authSuccess, failReason);
if (authSuccess) {
log.info("刷卡鉴权成功: pileCode={}, gunNo={}, cardNo={}, tradeNo={}",
pileCode, gunNo, cardNo, tradeNo);
} else {
log.warn("刷卡鉴权失败: pileCode={}, gunNo={}, cardNo={}, reason={}",
pileCode, gunNo, cardNo, failReason);
}
}
}

View File

@@ -0,0 +1,134 @@
package com.jsowell.pile.jcpp.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
/**
* JCPP 交易记录消息消费者
*
* @author jsowell
*/
@Slf4j
@Component
public class JcppTransactionConsumer {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Transactional(rollbackFor = Exception.class)
@RabbitListener(queues = JcppConstants.QUEUE_TRANSACTION)
public void handleTransaction(JcppUplinkMessage uplinkMessage) {
try {
// 验证消息
if (uplinkMessage == null || uplinkMessage.getData() == null) {
log.warn("交易记录消息格式错误");
return;
}
// 从 uplinkMessage 中获取 pileCode
String pileCode = uplinkMessage.getPileCode();
if (pileCode == null || pileCode.isEmpty()) {
log.warn("交易记录消息缺少 pileCode");
return;
}
log.info("收到 JCPP 交易记录消息: pileCode={}, messageType={}", pileCode, uplinkMessage.getMessageType());
// 从 data 中获取其他信息
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
Long startTs = data.getLong("startTs");
Long endTs = data.getLong("endTs");
String totalEnergyKWh = data.getString("totalEnergyKWh");
String totalAmountYuan = data.getString("totalAmountYuan");
String stopReason = data.getString("stopReason");
JSONObject detail = data.getJSONObject("detail");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("交易记录消息缺少 tradeNo");
return;
}
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, false);
return;
}
// 幂等性检查:避免重复处理
if (order.getOrderStatus() != null && StringUtils.equals(order.getOrderStatus(), "2")) {
log.info("订单已处理,跳过: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
return;
}
// 更新订单信息
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("2"); // 充电完成
if (startTs != null) {
updateOrder.setChargeStartTime(new Date(startTs));
}
if (endTs != null) {
updateOrder.setChargeEndTime(new Date(endTs));
}
if (totalEnergyKWh != null) {
// updateOrder.setTotalElectricity(new BigDecimal(totalEnergyKWh));
}
if (totalAmountYuan != null) {
updateOrder.setOrderAmount(new BigDecimal(totalAmountYuan));
}
if (stopReason != null) {
updateOrder.setReason(stopReason);
}
// 保存充电明细数据
if (detail != null) {
// updateOrder.setChargeDetail(detail.toJSONString());
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
log.info("更新订单完成: tradeNo={}, totalEnergy={}kWh, totalAmount={}元",
tradeNo, totalEnergyKWh, totalAmountYuan);
// 更新枪状态为空闲
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "1");
// 发送交易记录应答
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
// TODO: 触发结算流程
// orderBasicInfoService.realTimeOrderSplit(order.getId());
} catch (Exception e) {
log.error("处理 JCPP 交易记录消息异常: pileCode={}",
uplinkMessage != null ? uplinkMessage.getPileCode() : "unknown", e);
}
}
}

View File

@@ -0,0 +1,48 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 下行指令通用结构
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppDownlinkCommand implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 指令ID
*/
private String commandId;
/**
* 指令类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.DownlinkCommand
*/
private String commandType;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 时间戳
*/
private Long timestamp;
/**
* 指令数据
*/
private Object data;
}

View File

@@ -0,0 +1,53 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 下行请求
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppDownlinkRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private String sessionId;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 指令类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.DownlinkCommand
*/
private String commandType;
/**
* 指令数据
*/
private Object data;
/**
* 请求ID用于追踪
*/
private String requestId;
/**
* 时间戳
*/
private Long timestamp;
}

View File

@@ -0,0 +1,63 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* JCPP 登录消息数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppLoginData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 认证凭证
*/
private String credential;
/**
* 远程地址
*/
private String remoteAddress;
/**
* JCPP 节点ID
*/
private String nodeId;
/**
* JCPP 节点主机地址
*/
private String nodeHostAddress;
/**
* JCPP 节点 REST 端口
*/
private Integer nodeRestPort;
/**
* JCPP 节点 gRPC 端口
*/
private Integer nodeGrpcPort;
/**
* 附加信息
*/
private Map<String, Object> additionalInfo;
}

View File

@@ -0,0 +1,180 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* JCPP 计费模板
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppPricingModel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 计费模板ID
*/
private Long pricingId;
/**
* 计费模板名称
*/
private String pricingName;
/**
* 计费类型1-标准计费 2-峰谷计费 3-时段计费
*/
private Integer pricingType;
/**
* 电费单价(标准计费时使用)
*/
private BigDecimal electricityPrice;
/**
* 服务费单价(标准计费时使用)
*/
private BigDecimal servicePrice;
/**
* 时段计费明细
*/
private List<TimePeriodPrice> timePeriodPrices;
/**
* 峰谷计费明细
*/
private PeakValleyPrice peakValleyPrice;
/**
* 时段计费明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodPrice implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间格式HH:mm
*/
private String startTime;
/**
* 结束时间格式HH:mm
*/
private String endTime;
/**
* 电费单价
*/
private BigDecimal electricityPrice;
/**
* 服务费单价
*/
private BigDecimal servicePrice;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
/**
* 峰谷计费明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PeakValleyPrice implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 尖时电费
*/
private BigDecimal sharpElectricityPrice;
/**
* 尖时服务费
*/
private BigDecimal sharpServicePrice;
/**
* 峰时电费
*/
private BigDecimal peakElectricityPrice;
/**
* 峰时服务费
*/
private BigDecimal peakServicePrice;
/**
* 平时电费
*/
private BigDecimal flatElectricityPrice;
/**
* 平时服务费
*/
private BigDecimal flatServicePrice;
/**
* 谷时电费
*/
private BigDecimal valleyElectricityPrice;
/**
* 谷时服务费
*/
private BigDecimal valleyServicePrice;
/**
* 时段配置
*/
private List<TimePeriodConfig> timePeriodConfigs;
}
/**
* 时段配置
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间格式HH:mm
*/
private String startTime;
/**
* 结束时间格式HH:mm
*/
private String endTime;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
}

View File

@@ -0,0 +1,82 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 实时数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppRealTimeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 输出电压V
*/
private String outputVoltage;
/**
* 输出电流A
*/
private String outputCurrent;
/**
* SOC 百分比
*/
private Integer soc;
/**
* 充电时长(分钟)
*/
private Integer totalChargingDurationMin;
/**
* 充电电量kWh
*/
private String totalChargingEnergyKWh;
/**
* 充电费用(元)
*/
private String totalChargingCostYuan;
/**
* 剩余充电时间(分钟)
*/
private Integer remainingTimeMin;
/**
* 枪状态
*/
private String gunStatus;
/**
* 充电功率kW
*/
private String chargingPowerKW;
}

View File

@@ -0,0 +1,47 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 远程启动结果数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppRemoteStartResultData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 是否成功
*/
private Boolean success;
/**
* 失败原因
*/
private String failReason;
}

View File

@@ -0,0 +1,77 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 会话信息
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppSessionInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private String sessionId;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 远程地址
*/
private String remoteAddress;
/**
* JCPP 节点ID
*/
private String nodeId;
/**
* JCPP 节点主机地址
*/
private String nodeHostAddress;
/**
* JCPP 节点 REST 端口
*/
private Integer nodeRestPort;
/**
* JCPP 节点 gRPC 端口
*/
private Integer nodeGrpcPort;
/**
* 协议名称
*/
private String protocolName;
/**
* 登录时间戳
*/
private Long loginTimestamp;
/**
* 最后活跃时间戳
*/
private Long lastActiveTimestamp;
/**
* 是否在线
*/
private Boolean online;
}

View File

@@ -0,0 +1,62 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 刷卡/扫码启动充电数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppStartChargeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 启动类型CARD-刷卡, APP-APP/小程序, VIN-VIN码
*/
private String startType;
/**
* 卡号或账号(逻辑卡号)
*/
private String cardNo;
/**
* 物理卡号
*/
private String physicalCardNo;
/**
* 是否需要密码
*/
private Boolean needPassword;
/**
* 密码
*/
private String password;
/**
* 车辆VIN码
*/
private String carVinCode;
}

View File

@@ -0,0 +1,190 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* JCPP 交易记录数据
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppTransactionData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 枪编号
*/
private String gunNo;
/**
* 交易流水号
*/
private String tradeNo;
/**
* 开始时间戳
*/
private Long startTs;
/**
* 结束时间戳
*/
private Long endTs;
/**
* 总电量kWh
*/
private String totalEnergyKWh;
/**
* 总金额(元)- 可选,如果充电桩不上报则由平台计算
*/
private String totalAmountYuan;
/**
* 交易时间戳
*/
private Long tradeTs;
/**
* 停止原因
*/
private String stopReason;
/**
* 电量明细
*/
private EnergyDetail detail;
/**
* 电量明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class EnergyDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 明细类型PEAK_VALLEY-峰谷计费, TIME_PERIOD-时段计费
*/
private String type;
/**
* 峰谷明细
*/
private PeakValleyDetail peakValley;
/**
* 时段明细列表
*/
private List<TimePeriodDetail> timePeriods;
}
/**
* 峰谷明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PeakValleyDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 尖时电量kWh
*/
private BigDecimal sharpEnergyKWh;
/**
* 尖时金额(元)
*/
private BigDecimal sharpAmountYuan;
/**
* 峰时电量kWh
*/
private BigDecimal peakEnergyKWh;
/**
* 峰时金额(元)
*/
private BigDecimal peakAmountYuan;
/**
* 平时电量kWh
*/
private BigDecimal flatEnergyKWh;
/**
* 平时金额(元)
*/
private BigDecimal flatAmountYuan;
/**
* 谷时电量kWh
*/
private BigDecimal valleyEnergyKWh;
/**
* 谷时金额(元)
*/
private BigDecimal valleyAmountYuan;
}
/**
* 时段明细
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TimePeriodDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始时间HH:mm
*/
private String startTime;
/**
* 结束时间HH:mm
*/
private String endTime;
/**
* 电量kWh
*/
private BigDecimal energyKWh;
/**
* 金额(元)
*/
private BigDecimal amountYuan;
/**
* 时段类型1-尖 2-峰 3-平 4-谷
*/
private Integer periodType;
}
}

View File

@@ -0,0 +1,58 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 上行消息
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppUplinkMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息ID
*/
private String messageId;
/**
* 会话ID
*/
private String sessionId;
/**
* 协议名称
*/
private String protocolName;
/**
* 充电桩编码
*/
private String pileCode;
/**
* 消息类型
* @see com.jsowell.pile.jcpp.constant.JcppConstants.MessageType
*/
private String messageType;
/**
* 时间戳
*/
private Long timestamp;
/**
* 具体消息内容JSON 字符串格式,根据 messageType 不同,结构不同)
*/
private String data;
}

View File

@@ -0,0 +1,96 @@
package com.jsowell.pile.jcpp.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* JCPP 上行消息响应
*
* @author jsowell
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JcppUplinkResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
/**
* 响应数据
*/
private Object data;
/**
* 消息ID原样返回
*/
private String messageId;
/**
* 成功响应
*/
public static JcppUplinkResponse success() {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.build();
}
/**
* 成功响应(带数据)
*/
public static JcppUplinkResponse success(Object data) {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.data(data)
.build();
}
/**
* 成功响应带消息ID
*/
public static JcppUplinkResponse success(String messageId, Object data) {
return JcppUplinkResponse.builder()
.success(true)
.message("success")
.messageId(messageId)
.data(data)
.build();
}
/**
* 失败响应
*/
public static JcppUplinkResponse error(String message) {
return JcppUplinkResponse.builder()
.success(false)
.message(message)
.build();
}
/**
* 失败响应带消息ID
*/
public static JcppUplinkResponse error(String messageId, String message) {
return JcppUplinkResponse.builder()
.success(false)
.message(message)
.messageId(messageId)
.build();
}
}

View File

@@ -0,0 +1,43 @@
package com.jsowell.pile.jcpp.dto.sync;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import java.io.Serializable;
/**
* JCPP 充电枪同步数据传输对象
*
* @author jsowell
*/
@Data
public class JcppGunSyncDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电枪编码(对应 Web 的 pile_connector_code
*/
private String gunCode;
/**
* 充电枪名称(对应 Web 的 name
*/
private String gunName;
/**
* 枪号(从 gunCode 提取最后 2 位)
*/
private String gunNo;
/**
* 所属充电桩编码(对应 Web 的 pile_sn
*/
private String pileCode;
/**
* 附加信息JSON 格式)
* 包含webGunId, status, parkNo 等
*/
private JSONObject additionalInfo;
}

View File

@@ -0,0 +1,58 @@
package com.jsowell.pile.jcpp.dto.sync;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import java.io.Serializable;
/**
* JCPP 充电桩同步数据传输对象
*
* @author jsowell
*/
@Data
public class JcppPileSyncDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码(对应 Web 的 sn
*/
private String pileCode;
/**
* 充电桩名称(对应 Web 的 name
*/
private String pileName;
/**
* 软件协议(对应 Web 的 software_protocol
*/
private String protocol;
/**
* 品牌
*/
private String brand;
/**
* 型号
*/
private String model;
/**
* 制造商
*/
private String manufacturer;
/**
* 类型OPERATION-运营桩, PERSONAL-个人桩
*/
private String type;
/**
* 附加信息JSON 格式)
* 包含webPileId, webStationId, businessType, secretKey, longitude, latitude, iccid 等
*/
private JSONObject additionalInfo;
}

View File

@@ -0,0 +1,38 @@
package com.jsowell.pile.jcpp.dto.sync;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* JCPP 同步请求
*
* @author jsowell
*/
@Data
public class JcppSyncRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 同步类型FULL-全量, INCREMENTAL-增量
*/
private String syncType;
/**
* 上次同步时间(增量同步时使用)
*/
private Date lastSyncTime;
/**
* 充电桩列表
*/
private List<JcppPileSyncDTO> piles;
/**
* 充电枪列表
*/
private List<JcppGunSyncDTO> guns;
}

View File

@@ -0,0 +1,139 @@
package com.jsowell.pile.jcpp.dto.sync;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* JCPP 同步响应
*
* @author jsowell
*/
@Data
public class JcppSyncResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否成功
*/
private Boolean success;
/**
* 消息
*/
private String message;
/**
* 总充电桩数
*/
private Integer totalPiles;
/**
* 成功充电桩数
*/
private Integer successPiles;
/**
* 失败充电桩数
*/
private Integer failedPiles;
/**
* 总充电枪数
*/
private Integer totalGuns;
/**
* 成功充电枪数
*/
private Integer successGuns;
/**
* 失败充电枪数
*/
private Integer failedGuns;
/**
* 同步结果列表
*/
private List<JcppSyncResult> results;
/**
* 错误信息列表
*/
private List<String> errors;
/**
* 构建响应
*/
public static JcppSyncResponse build(List<JcppSyncResult> pileResults, List<JcppSyncResult> gunResults) {
JcppSyncResponse response = new JcppSyncResponse();
// 统计充电桩结果
int totalPiles = pileResults != null ? pileResults.size() : 0;
int successPiles = 0;
int failedPiles = 0;
if (pileResults != null) {
for (JcppSyncResult result : pileResults) {
if (Boolean.TRUE.equals(result.getSuccess())) {
successPiles++;
} else {
failedPiles++;
}
}
}
// 统计充电枪结果
int totalGuns = gunResults != null ? gunResults.size() : 0;
int successGuns = 0;
int failedGuns = 0;
if (gunResults != null) {
for (JcppSyncResult result : gunResults) {
if (Boolean.TRUE.equals(result.getSuccess())) {
successGuns++;
} else {
failedGuns++;
}
}
}
// 设置统计信息
response.setTotalPiles(totalPiles);
response.setSuccessPiles(successPiles);
response.setFailedPiles(failedPiles);
response.setTotalGuns(totalGuns);
response.setSuccessGuns(successGuns);
response.setFailedGuns(failedGuns);
// 合并结果
List<JcppSyncResult> allResults = new ArrayList<>();
if (pileResults != null) {
allResults.addAll(pileResults);
}
if (gunResults != null) {
allResults.addAll(gunResults);
}
response.setResults(allResults);
// 收集错误信息
List<String> errors = new ArrayList<>();
for (JcppSyncResult result : allResults) {
if (Boolean.FALSE.equals(result.getSuccess())) {
errors.add(result.getCode() + ": " + result.getMessage());
}
}
response.setErrors(errors);
// 判断整体是否成功
boolean overallSuccess = (failedPiles == 0 && failedGuns == 0);
response.setSuccess(overallSuccess);
response.setMessage(overallSuccess ? "同步成功" : "同步部分失败");
return response;
}
}

View File

@@ -0,0 +1,63 @@
package com.jsowell.pile.jcpp.dto.sync;
import lombok.Data;
import java.io.Serializable;
/**
* JCPP 单个同步结果
*
* @author jsowell
*/
@Data
public class JcppSyncResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 充电桩编码或充电枪编码
*/
private String code;
/**
* JCPP 返回的 IDUUID
*/
private String id;
/**
* 是否成功
*/
private Boolean success;
/**
* 消息
*/
private String message;
/**
* 创建成功结果
*/
public static JcppSyncResult success(String code, String id, String message) {
JcppSyncResult result = new JcppSyncResult();
result.setCode(code);
result.setId(id);
result.setSuccess(true);
result.setMessage(message);
return result;
}
/**
* 创建失败结果
*/
public static JcppSyncResult fail(String code, String message) {
JcppSyncResult result = new JcppSyncResult();
result.setCode(code);
result.setSuccess(false);
result.setMessage(message);
return result;
}
public Boolean isSuccess() {
return success;
}
}

View File

@@ -0,0 +1,23 @@
package com.jsowell.pile.jcpp.service;
/**
* JCPP 认证服务接口
*
* @author jsowell
*/
public interface IJcppAuthService {
/**
* 获取 JCPP 访问令牌
* 如果 Redis 中有缓存且未过期,直接返回
* 否则调用登录接口获取新的 token
*
* @return 访问令牌
*/
String getAccessToken();
/**
* 清除缓存的令牌(用于强制刷新)
*/
void clearToken();
}

View File

@@ -0,0 +1,173 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import java.util.List;
import java.util.Map;
/**
* JCPP 下行调用服务接口
*
* @author jsowell
*/
public interface IJcppDownlinkService {
/**
* 发送登录应答
*
* @param pileCode 充电桩编码
* @param success 是否登录成功
*/
void sendLoginAck(String pileCode, boolean success);
/**
* 发送远程启动充电指令
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param tradeNo 交易流水号
* @param limitYuan 限制金额(元)
* @param logicalCardNo 逻辑卡号
* @param physicalCardNo 物理卡号
*/
void sendRemoteStartCharging(String pileCode, String gunNo, String tradeNo,
String limitYuan, String logicalCardNo, String physicalCardNo);
/**
* 发送远程停止充电指令
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
*/
void sendRemoteStopCharging(String pileCode, String gunNo);
/**
* 发送计费模板
*
* @param pileCode 充电桩编码
* @param pricingId 计费模板ID
* @param pricingModel 计费模板
*/
void sendSetPricing(String pileCode, Long pricingId, Object pricingModel);
/**
* 发送查询计费应答
*
* @param pileCode 充电桩编码
* @param pricingId 计费模板ID
* @param pricingModel 计费模板
*/
void sendQueryPricingAck(String pileCode, Long pricingId, Object pricingModel);
/**
* 发送校验计费应答
*
* @param pileCode 充电桩编码
* @param success 是否校验成功
* @param pricingId 计费模板ID
*/
void sendVerifyPricingAck(String pileCode, boolean success, Long pricingId);
/**
* 发送启动充电应答(刷卡鉴权结果)
*
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param tradeNo 交易流水号
* @param logicalCardNo 逻辑卡号
* @param limitYuan 限制金额(元)
* @param authSuccess 鉴权是否成功
* @param failReason 失败原因
*/
void sendStartChargeAck(String pileCode, String gunNo, String tradeNo,
String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason);
/**
* 发送交易记录应答
*
* @param pileCode 充电桩编码
* @param tradeNo 交易流水号
* @param success 是否成功
*/
void sendTransactionRecordAck(String pileCode, String tradeNo, boolean success);
/**
* 检查充电桩是否在线
*
* @param pileCode 充电桩编码
* @return 是否在线
*/
boolean isPileOnline(String pileCode);
/**
* 获取充电桩会话信息
*
* @param pileCode 充电桩编码
* @return 会话信息
*/
Map<String, String> getSessionInfo(String pileCode);
// ==================== 兼容旧接口 ====================
/**
* 远程启动充电(兼容旧接口)
*/
boolean remoteStartCharging(String sessionId, String pileCode, String gunNo,
String tradeNo, String limitYuan,
String logicalCardNo, String physicalCardNo);
/**
* 远程停止充电(兼容旧接口)
*/
boolean remoteStopCharging(String sessionId, String pileCode, String gunNo);
/**
* 下发计费模板(兼容旧接口)
*/
boolean setPricing(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel);
/**
* 查询计费应答(兼容旧接口)
*/
boolean queryPricingAck(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel);
/**
* 校验计费应答(兼容旧接口)
*/
boolean verifyPricingAck(String sessionId, String pileCode, boolean success, Long pricingId);
/**
* 登录应答(兼容旧接口)
*/
boolean loginAck(String sessionId, String pileCode, boolean success);
/**
* 启动充电应答(兼容旧接口)
*/
boolean startChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason);
/**
* 交易记录应答(兼容旧接口)
*/
boolean transactionRecordAck(String sessionId, String tradeNo, boolean success);
/**
* 查询充电桩会话信息(兼容旧接口)
*/
JcppSessionInfo getSession(String pileCode);
/**
* 查询所有在线充电桩(兼容旧接口)
*/
List<JcppSessionInfo> getSessions();
/**
* 发送通用下行指令(兼容旧接口)
*/
Map<String, Object> sendDownlinkCommand(String sessionId, String pileCode,
String commandType, Object data);
}

View File

@@ -0,0 +1,19 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
/**
* JCPP JSON 消息处理器接口
* 处理从 JCPP 接收到的各种上行消息JSON 格式)
*
* @author jsowell
*/
public interface IJcppJsonMessageHandler {
/**
* 处理上行消息
*
* @param message JSON 上行消息
*/
void handleUplinkMessage(JcppUplinkMessage message);
}

View File

@@ -0,0 +1,100 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.dto.JcppUplinkResponse;
/**
* JCPP 消息处理服务接口
*
* @author jsowell
*/
public interface IJcppMessageService {
/**
* 处理上行消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleMessage(JcppUplinkMessage message);
/**
* 处理登录消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleLogin(JcppUplinkMessage message);
/**
* 处理心跳消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleHeartbeat(JcppUplinkMessage message);
/**
* 处理刷卡/扫码启动充电消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleStartCharge(JcppUplinkMessage message);
/**
* 处理实时数据上报消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRealTimeData(JcppUplinkMessage message);
/**
* 处理交易记录消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleTransactionRecord(JcppUplinkMessage message);
/**
* 处理枪状态变化消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleGunStatus(JcppUplinkMessage message);
/**
* 处理校验计费模板消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleVerifyPricing(JcppUplinkMessage message);
/**
* 处理查询计费模板消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleQueryPricing(JcppUplinkMessage message);
/**
* 处理远程启动结果消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRemoteStartResult(JcppUplinkMessage message);
/**
* 处理远程停止结果消息
*
* @param message 上行消息
* @return 响应
*/
JcppUplinkResponse handleRemoteStopResult(JcppUplinkMessage message);
}

View File

@@ -0,0 +1,36 @@
package com.jsowell.pile.jcpp.service;
import com.jsowell.pile.jcpp.dto.sync.JcppSyncResponse;
import java.util.Date;
/**
* JCPP 充电桩同步服务接口
*
* @author jsowell
*/
public interface IJcppPileSyncService {
/**
* 全量同步充电桩数据到 JCPP
*
* @return 同步结果
*/
JcppSyncResponse syncAllPiles();
/**
* 增量同步充电桩数据到 JCPP
*
* @param lastSyncTime 上次同步时间(可选,如果为 null 则查询最后一次成功的同步记录)
* @return 同步结果
*/
JcppSyncResponse syncIncrementalPiles(Date lastSyncTime);
/**
* 同步单个充电桩
*
* @param pileSn 充电桩编号
* @return 是否成功
*/
boolean syncSinglePile(String pileSn);
}

View File

@@ -0,0 +1,46 @@
package com.jsowell.pile.jcpp.service;
/**
* JCPP 远程充电服务接口
* 用于 APP/小程序发起的远程启动/停止充电
*
* @author jsowell
*/
public interface IJcppRemoteChargeService {
/**
* 远程启动充电
*
* @param memberId 会员ID
* @param pileCode 充电桩编码
* @param gunNo 枪编号
* @param payAmount 预付金额(元)
* @return 订单号
*/
String remoteStartCharging(String memberId, String pileCode, String gunNo, String payAmount);
/**
* 远程停止充电
*
* @param memberId 会员ID
* @param orderCode 订单号
* @return 是否成功
*/
boolean remoteStopCharging(String memberId, String orderCode);
/**
* 检查充电桩是否在线
*
* @param pileCode 充电桩编码
* @return 是否在线
*/
boolean isPileOnline(String pileCode);
/**
* 获取充电桩会话ID
*
* @param pileCode 充电桩编码
* @return 会话ID
*/
String getSessionId(String pileCode);
}

View File

@@ -0,0 +1,124 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.pile.jcpp.service.IJcppAuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.TimeUnit;
/**
* JCPP 认证服务实现
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppAuthServiceImpl implements IJcppAuthService {
private static final String JCPP_TOKEN_KEY = "jcpp:auth:token";
private static final long TOKEN_EXPIRE_MINUTES = 30L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RestTemplate restTemplate;
@Value("${jcpp.sync.api-url:http://localhost:8180/api/sync}")
private String jcppApiUrl;
@Value("${jcpp.auth.username:sanbing}")
private String username;
@Value("${jcpp.auth.password:password123}")
private String password;
/**
* 获取 JCPP 访问令牌
*/
@Override
public String getAccessToken() {
// 1. 尝试从 Redis 获取缓存的 token
String cachedToken = stringRedisTemplate.opsForValue().get(JCPP_TOKEN_KEY);
if (cachedToken != null && !cachedToken.isEmpty()) {
log.debug("使用缓存的 JCPP token");
return cachedToken;
}
// 2. 缓存中没有,调用登录接口获取新的 token
log.info("缓存中没有 token调用登录接口获取");
String token = login();
// 3. 将 token 缓存到 Redis有效期 30 分钟
if (token != null && !token.isEmpty()) {
stringRedisTemplate.opsForValue().set(JCPP_TOKEN_KEY, token, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);
log.info("JCPP token 已缓存,有效期 {} 分钟", TOKEN_EXPIRE_MINUTES);
}
return token;
}
/**
* 清除缓存的令牌
*/
@Override
public void clearToken() {
stringRedisTemplate.delete(JCPP_TOKEN_KEY);
log.info("已清除缓存的 JCPP token");
}
/**
* 调用 JCPP 登录接口
*/
private String login() {
// 构建登录 URL从 api-url 中提取基础 URL
String baseUrl = jcppApiUrl.replace("/api/sync", "");
String loginUrl = baseUrl + "/api/auth/login";
try {
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("username", username);
requestBody.put("password", password);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
log.info("调用 JCPP 登录接口: {}", loginUrl);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(loginUrl, entity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
// 解析响应,提取 token
JSONObject responseBody = JSON.parseObject(response.getBody());
String token = responseBody.getString("token");
if (token != null && !token.isEmpty()) {
log.info("JCPP 登录成功,获取到 token");
return token;
} else {
log.error("JCPP 登录响应中没有 token: {}", response.getBody());
throw new RuntimeException("登录响应中没有 token");
}
} else {
log.error("JCPP 登录失败,状态码: ", response.getStatusCode());
throw new RuntimeException("登录失败,状态码: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("调用 JCPP 登录接口异常", e);
throw new RuntimeException("登录失败: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,432 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.jsowell.pile.jcpp.config.JcppConfig;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppDownlinkCommand;
import com.jsowell.pile.jcpp.dto.JcppDownlinkRequest;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
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 java.util.*;
import java.util.concurrent.TimeUnit;
/**
* JCPP 下行调用服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppDownlinkServiceImpl implements IJcppDownlinkService {
@Autowired
private JcppConfig jcppConfig;
@Autowired
@Qualifier("jcppRestTemplate")
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// ==================== 新接口实现(基于 Redis 队列) ====================
@Override
public void sendLoginAck(String pileCode, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
sendCommand(pileCode, JcppConstants.DownlinkCommand.LOGIN_ACK, data);
}
@Override
public void sendRemoteStartCharging(String pileCode, String gunNo, String tradeNo,
String limitYuan, String logicalCardNo, String physicalCardNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("limitYuan", limitYuan);
data.put("logicalCardNo", logicalCardNo);
data.put("physicalCardNo", physicalCardNo);
sendCommand(pileCode, JcppConstants.DownlinkCommand.REMOTE_START, data);
}
@Override
public void sendRemoteStopCharging(String pileCode, String gunNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
sendCommand(pileCode, JcppConstants.DownlinkCommand.REMOTE_STOP, data);
}
@Override
public void sendSetPricing(String pileCode, Long pricingId, Object pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
sendCommand(pileCode, JcppConstants.DownlinkCommand.SET_PRICING, data);
}
@Override
public void sendQueryPricingAck(String pileCode, Long pricingId, Object pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
sendCommand(pileCode, JcppConstants.DownlinkCommand.QUERY_PRICING_ACK, data);
}
@Override
public void sendVerifyPricingAck(String pileCode, boolean success, Long pricingId) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
data.put("pricingId", pricingId);
sendCommand(pileCode, JcppConstants.DownlinkCommand.VERIFY_PRICING_ACK, data);
}
@Override
public void sendStartChargeAck(String pileCode, String gunNo, String tradeNo,
String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("logicalCardNo", logicalCardNo);
data.put("limitYuan", limitYuan);
data.put("authSuccess", authSuccess);
data.put("failReason", failReason);
sendCommand(pileCode, JcppConstants.DownlinkCommand.START_CHARGE_ACK, data);
}
@Override
public void sendTransactionRecordAck(String pileCode, String tradeNo, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("tradeNo", tradeNo);
data.put("success", success);
sendCommand(pileCode, JcppConstants.DownlinkCommand.TRANSACTION_RECORD_ACK, data);
}
@Override
public boolean isPileOnline(String pileCode) {
Boolean isMember = stringRedisTemplate.opsForSet().isMember(JcppConstants.REDIS_ONLINE_PILES_KEY, pileCode);
return Boolean.TRUE.equals(isMember);
}
@Override
public Map<String, String> getSessionInfo(String pileCode) {
String key = JcppConstants.REDIS_SESSION_PREFIX + pileCode;
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
Map<String, String> result = new HashMap<>();
entries.forEach((k, v) -> result.put(String.valueOf(k), String.valueOf(v)));
return result;
}
/**
* 发送下行指令到 Redis 队列
*/
private void sendCommand(String pileCode, String commandType, Object data) {
JcppDownlinkCommand command = JcppDownlinkCommand.builder()
.commandId(UUID.randomUUID().toString())
.commandType(commandType)
.pileCode(pileCode)
.timestamp(System.currentTimeMillis())
.data(data)
.build();
String key = JcppConstants.REDIS_DOWNLINK_PREFIX + pileCode;
String json = JSON.toJSONString(command);
stringRedisTemplate.opsForList().rightPush(key, json);
log.info("发送 JCPP 下行指令: pileCode={}, commandType={}, commandId={}",
pileCode, commandType, command.getCommandId());
}
// ==================== 兼容旧接口实现(基于 HTTP ====================
@Override
public boolean remoteStartCharging(String sessionId, String pileCode, String gunNo,
String tradeNo, String limitYuan,
String logicalCardNo, String physicalCardNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("limitYuan", limitYuan);
data.put("logicalCardNo", logicalCardNo);
data.put("physicalCardNo", physicalCardNo);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.REMOTE_START, data);
return isSuccess(result);
}
@Override
public boolean remoteStopCharging(String sessionId, String pileCode, String gunNo) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.REMOTE_STOP, data);
return isSuccess(result);
}
@Override
public boolean setPricing(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.SET_PRICING, data);
return isSuccess(result);
}
@Override
public boolean queryPricingAck(String sessionId, String pileCode, Long pricingId, JcppPricingModel pricingModel) {
Map<String, Object> data = new HashMap<>();
data.put("pricingId", pricingId);
data.put("pricingModel", pricingModel);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.QUERY_PRICING_ACK, data);
return isSuccess(result);
}
@Override
public boolean verifyPricingAck(String sessionId, String pileCode, boolean success, Long pricingId) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
data.put("pricingId", pricingId);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.VERIFY_PRICING_ACK, data);
return isSuccess(result);
}
@Override
public boolean loginAck(String sessionId, String pileCode, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("success", success);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.LOGIN_ACK, data);
return isSuccess(result);
}
@Override
public boolean startChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String logicalCardNo, String limitYuan,
boolean authSuccess, String failReason) {
Map<String, Object> data = new HashMap<>();
data.put("gunNo", gunNo);
data.put("tradeNo", tradeNo);
data.put("logicalCardNo", logicalCardNo);
data.put("limitYuan", limitYuan);
data.put("authSuccess", authSuccess);
data.put("failReason", failReason);
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.START_CHARGE_ACK, data);
return isSuccess(result);
}
@Override
public boolean transactionRecordAck(String sessionId, String tradeNo, boolean success) {
Map<String, Object> data = new HashMap<>();
data.put("tradeNo", tradeNo);
data.put("success", success);
// 从 Redis 获取 pileCode
String pileCode = getPileCodeByTradeNo(tradeNo);
if (pileCode == null) {
log.warn("无法获取交易流水号对应的充电桩编码, tradeNo: {}", tradeNo);
return false;
}
Map<String, Object> result = sendDownlinkCommand(sessionId, pileCode,
JcppConstants.DownlinkCommand.TRANSACTION_RECORD_ACK, data);
return isSuccess(result);
}
@Override
public JcppSessionInfo getSession(String pileCode) {
// 先从 Redis 缓存获取
String cacheKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (sessionJson != null) {
return JSON.parseObject(sessionJson, JcppSessionInfo.class);
}
// 从 JCPP 服务获取
try {
String url = jcppConfig.getSessionUrl() + "/" + pileCode;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
if (Boolean.TRUE.equals(result.get("success"))) {
Object data = result.get("data");
if (data != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(JSON.toJSONString(data), JcppSessionInfo.class);
// 缓存到 Redis
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
return sessionInfo;
}
}
}
} catch (RestClientException e) {
log.error("获取 JCPP 会话信息失败, pileCode: {}, error: {}", pileCode, e.getMessage());
}
return null;
}
@Override
public List<JcppSessionInfo> getSessions() {
try {
String url = jcppConfig.getSessionUrl();
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
if (Boolean.TRUE.equals(result.get("success"))) {
Object data = result.get("data");
if (data != null) {
return JSON.parseArray(JSON.toJSONString(data), JcppSessionInfo.class);
}
}
}
} catch (RestClientException e) {
log.error("获取 JCPP 所有会话信息失败, error: {}", e.getMessage());
}
return Collections.emptyList();
}
@Override
public Map<String, Object> sendDownlinkCommand(String sessionId, String pileCode,
String commandType, Object data) {
if (!jcppConfig.isEnabled()) {
log.warn("JCPP 对接未启用,忽略下行指令: commandType={}, pileCode={}", commandType, pileCode);
return createErrorResult("JCPP 对接未启用");
}
// 如果没有传入 sessionId尝试从 Redis 获取
if (sessionId == null || sessionId.isEmpty()) {
sessionId = getSessionIdFromRedis(pileCode);
if (sessionId == null) {
log.warn("无法获取充电桩会话ID, pileCode: {}", pileCode);
return createErrorResult("充电桩不在线");
}
}
JcppDownlinkRequest request = JcppDownlinkRequest.builder()
.sessionId(sessionId)
.pileCode(pileCode)
.commandType(commandType)
.data(data)
.requestId(UUID.randomUUID().toString())
.timestamp(System.currentTimeMillis())
.build();
return sendWithRetry(request);
}
/**
* 带重试的发送
*/
private Map<String, Object> sendWithRetry(JcppDownlinkRequest request) {
int retryCount = jcppConfig.getRetryCount();
int retryInterval = jcppConfig.getRetryInterval();
for (int i = 0; i <= retryCount; i++) {
try {
return doSend(request);
} catch (RestClientException e) {
log.warn("发送 JCPP 下行指令失败, 第 {} 次尝试, requestId: {}, error: {}",
i + 1, request.getRequestId(), e.getMessage());
if (i < retryCount) {
try {
Thread.sleep(retryInterval);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
return createErrorResult("发送下行指令失败,已重试 " + retryCount + "");
}
/**
* 实际发送
*/
private Map<String, Object> doSend(JcppDownlinkRequest request) {
String url = jcppConfig.getDownlinkUrl();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<JcppDownlinkRequest> entity = new HttpEntity<>(request, headers);
log.info("发送 JCPP 下行指令: url=, requestId={}, commandType={}, pileCode={}",
url, request.getRequestId(), request.getCommandType(), request.getPileCode());
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> result = JSON.parseObject(response.getBody(),
new TypeReference<Map<String, Object>>() {});
log.info("JCPP 下行指令响应: requestId={}, result={}", request.getRequestId(), result);
return result;
}
return createErrorResult("HTTP 响应异常: " + response.getStatusCode());
}
/**
* 从 Redis 获取会话ID
*/
private String getSessionIdFromRedis(String pileCode) {
String cacheKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
return sessionInfo.getSessionId();
}
return null;
}
/**
* 根据交易流水号获取充电桩编码
*/
private String getPileCodeByTradeNo(String tradeNo) {
// TODO: 从订单表或 Redis 中获取
return null;
}
/**
* 判断结果是否成功
*/
private boolean isSuccess(Map<String, Object> result) {
return result != null && Boolean.TRUE.equals(result.get("success"));
}
/**
* 创建错误结果
*/
private Map<String, Object> createErrorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", message);
return result;
}
}

View File

@@ -0,0 +1,645 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.enums.ykc.StartTypeEnum;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.id.IdUtils;
import com.jsowell.pile.domain.*;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.jcpp.dto.JcppUplinkMessage;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.service.IJcppJsonMessageHandler;
import com.jsowell.pile.jcpp.util.PricingModelConverter;
import com.jsowell.pile.service.*;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* JCPP JSON 消息处理器实现
* 整合原有各个消费者的处理逻辑,支持分区消费
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppJsonMessageHandlerImpl implements IJcppJsonMessageHandler {
private static final String HEARTBEAT_KEY_PREFIX = "jcpp:heartbeat:";
private static final long HEARTBEAT_EXPIRE_SECONDS = 180L;
private static final String REALTIME_DATA_KEY_PREFIX = "jcpp:realtime:";
private static final long REALTIME_DATA_EXPIRE_SECONDS = 300L;
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private PileAuthCardService pileAuthCardService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private PileStationWhitelistService pileStationWhitelistService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void handleUplinkMessage(JcppUplinkMessage uplinkMessage) {
String messageType = uplinkMessage.getMessageType();
String pileCode = uplinkMessage.getPileCode();
log.debug("处理上行消息: pileCode={}, messageType={}", pileCode, messageType);
// 根据消息类型分发处理
switch (messageType) {
case JcppConstants.MessageType.LOGIN:
handleLogin(uplinkMessage);
break;
case JcppConstants.MessageType.HEARTBEAT:
handleHeartbeat(uplinkMessage);
break;
case JcppConstants.MessageType.GUN_STATUS:
handleGunStatus(uplinkMessage);
break;
case JcppConstants.MessageType.REAL_TIME_DATA:
handleRealTimeData(uplinkMessage);
break;
case JcppConstants.MessageType.TRANSACTION_RECORD:
handleTransactionRecord(uplinkMessage);
break;
case JcppConstants.MessageType.START_CHARGE:
handleStartCharge(uplinkMessage);
break;
case JcppConstants.MessageType.QUERY_PRICING:
handleQueryPricing(uplinkMessage);
break;
case JcppConstants.MessageType.VERIFY_PRICING:
handleVerifyPricing(uplinkMessage);
break;
case JcppConstants.MessageType.SESSION_CLOSE:
handleSessionClose(uplinkMessage);
break;
case JcppConstants.MessageType.REMOTE_START_RESULT:
case JcppConstants.MessageType.REMOTE_STOP_RESULT:
handleRemoteResult(uplinkMessage);
break;
default:
log.warn("未知的消息类型: messageType={}, pileCode={}", messageType, pileCode);
}
}
/**
* 处理登录请求
*/
private void handleLogin(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
log.info("处理登录请求: pileCode={}", pileCode);
// 查询充电桩是否存在
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
boolean exists = pileInfo != null;
if (exists) {
log.info("充电桩登录成功: pileCode={}", pileCode);
} else {
log.warn("充电桩不存在: pileCode={}", pileCode);
}
// 发送登录应答
jcppDownlinkService.sendLoginAck(pileCode, exists);
}
/**
* 处理心跳请求
*/
private void handleHeartbeat(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
// 更新最后活跃时间到 Redis
String key = HEARTBEAT_KEY_PREFIX + pileCode;
stringRedisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),
HEARTBEAT_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.debug("收到充电桩心跳: pileCode={}", pileCode);
}
/**
* 处理枪状态上报
*/
private void handleGunStatus(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String gunRunStatus = data.getString("gunRunStatus");
log.info("处理枪状态: pileCode={}, gunNo={}, status={}", pileCode, gunNo, gunRunStatus);
// 映射状态
String systemStatus = mapGunStatus(gunRunStatus);
// 更新枪状态
String pileConnectorCode = pileCode + gunNo;
int result = pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, systemStatus);
if (result > 0) {
log.info("更新枪状态成功: pileConnectorCode={}, status={}", pileConnectorCode, systemStatus);
} else {
log.warn("更新枪状态失败: pileConnectorCode={}", pileConnectorCode);
}
// 记录故障信息
if (data.containsKey("faultMessages") && data.getJSONArray("faultMessages") != null) {
log.warn("充电枪故障: pileConnectorCode={}, faults={}", pileConnectorCode, data.getJSONArray("faultMessages"));
}
}
/**
* 处理充电进度数据
*/
private void handleRealTimeData(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
if (tradeNo == null || tradeNo.isEmpty()) {
log.debug("实时数据消息缺少 tradeNo");
return;
}
// 将实时数据缓存到 Redis
String key = REALTIME_DATA_KEY_PREFIX + tradeNo;
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(data),
REALTIME_DATA_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order != null && "1".equals(order.getOrderStatus())) {
// 更新订单实时数据
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
String totalChargingCostYuan = data.getString("totalChargingCostYuan");
if (totalChargingCostYuan != null && !totalChargingCostYuan.isEmpty()) {
updateOrder.setOrderAmount(new BigDecimal(totalChargingCostYuan));
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
}
// 更新枪状态为充电中
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
log.debug("处理充电进度: tradeNo={}, pileCode={}", tradeNo, pileCode);
}
/**
* 处理交易记录
*/
@Transactional(rollbackFor = Exception.class)
private void handleTransactionRecord(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String tradeNo = data.getString("tradeNo");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("交易记录消息缺少 tradeNo");
return;
}
log.info("处理交易记录: tradeNo={}, pileCode={}, gunNo={}", tradeNo, pileCode, gunNo);
// 根据 tradeNo 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, false);
return;
}
// 幂等性检查
if ("2".equals(order.getOrderStatus())) {
log.info("订单已处理,跳过: tradeNo={}", tradeNo);
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
return;
}
// 更新订单信息
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("2"); // 充电完成
Long startTs = data.getLong("startTs");
Long endTs = data.getLong("endTs");
if (startTs != null && startTs > 0) {
updateOrder.setChargeStartTime(new Date(startTs));
}
if (endTs != null && endTs > 0) {
updateOrder.setChargeEndTime(new Date(endTs));
}
String totalAmountYuan = data.getString("totalAmountYuan");
if (totalAmountYuan != null && !totalAmountYuan.isEmpty()) {
updateOrder.setOrderAmount(new BigDecimal(totalAmountYuan));
}
String stopReason = data.getString("stopReason");
if (stopReason != null && !stopReason.isEmpty()) {
updateOrder.setReason(stopReason);
}
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
log.info("更新订单完成: tradeNo={}", tradeNo);
// 更新枪状态为空闲
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "1");
// 发送交易记录应答
jcppDownlinkService.sendTransactionRecordAck(pileCode, tradeNo, true);
// TODO: 触发结算流程
// orderBasicInfoService.realTimeOrderSplit(order.getId());
}
/**
* 处理启动充电请求(刷卡)
*/
@Transactional(rollbackFor = Exception.class)
private void handleStartCharge(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String gunNo = data.getString("gunNo");
String startType = data.getString("startType");
String cardNo = data.getString("cardNo");
if (pileCode == null || gunNo == null) {
log.warn("启动充电消息缺少必要字段");
return;
}
log.info("处理启动充电请求: pileCode={}, gunNo={}, cardNo={}", pileCode, gunNo, cardNo);
// 处理刷卡启动
if ("CARD".equals(startType)) {
handleCardStartCharge(pileCode, gunNo, cardNo);
} else {
log.warn("不支持的启动类型: {}", startType);
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, null, cardNo, null,
false, "不支持的启动类型");
}
}
/**
* 处理刷卡启动充电
*/
private void handleCardStartCharge(String pileCode, String gunNo, String cardNo) {
String failReason = null;
String tradeNo = null;
String limitYuan = null;
boolean authSuccess = false;
try {
// 1. 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
failReason = "充电桩不存在";
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查充电桩状态
if (pileInfo.getDelFlag() != null && StringUtils.equals(pileInfo.getDelFlag(), "1")) {
failReason = "充电桩已停用";
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 2. 查询授权卡信息
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
if (authCard == null) {
failReason = "账户不存在";
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 检查卡状态
if (authCard.getStatus() != null && !StringUtils.equals(authCard.getStatus(), "1")) {
failReason = "账户已冻结";
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 3. 查询会员信息和钱包余额
String memberId = authCard.getMemberId();
BigDecimal balance = BigDecimal.ZERO;
if (memberId != null) {
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, String.valueOf(pileInfo.getMerchantId()));
if (walletInfo != null) {
balance = walletInfo.getPrincipalBalance();
if (walletInfo.getGiftBalance() != null) {
balance = balance.add(walletInfo.getGiftBalance());
}
}
}
// 4. 检查白名单
boolean isWhitelist = checkWhitelist(pileInfo.getStationId(), cardNo, memberId);
// 5. 验证余额(非白名单用户需要检查余额)
BigDecimal minAmount = new BigDecimal("1.00");
if (!isWhitelist && balance.compareTo(minAmount) < 0) {
failReason = "余额不足";
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, false, failReason);
return;
}
// 6. 生成交易流水号
tradeNo = IdUtils.fastSimpleUUID();
limitYuan = balance.toString();
// 7. 创建充电订单
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + gunNo);
order.setMemberId(memberId);
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setOrderStatus("0"); // 待支付/启动中
order.setPayMode(String.valueOf(isWhitelist ? 3 : 1)); // 3-白名单支付, 1-余额支付
order.setCreateTime(new Date());
order.setStartType(StartTypeEnum.NOW.getValue());
orderBasicInfoService.insert(order);
log.info("创建充电订单: tradeNo={}, pileCode={}, gunNo={}", tradeNo, pileCode, gunNo);
authSuccess = true;
} catch (Exception e) {
log.error("处理刷卡启动充电异常: pileCode={}, gunNo={}, cardNo={}", pileCode, gunNo, cardNo, e);
failReason = "系统错误";
}
// 发送鉴权结果
sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan, authSuccess, failReason);
}
/**
* 检查白名单
*/
private boolean checkWhitelist(Long stationId, String cardNo, String memberId) {
try {
if (stationId == null) {
return false;
}
PileStationWhitelist pileStationWhitelist = pileStationWhitelistService.queryWhitelistByMemberId(String.valueOf(stationId), memberId);
return pileStationWhitelist != null;
} catch (Exception e) {
log.error("检查白名单异常: stationId={}", stationId, e);
}
return false;
}
/**
* 发送启动充电应答
*/
private void sendStartChargeAck(String pileCode, String gunNo, String tradeNo, String cardNo,
String limitYuan, boolean authSuccess, String failReason) {
jcppDownlinkService.sendStartChargeAck(pileCode, gunNo, tradeNo, cardNo, limitYuan,
authSuccess, failReason);
if (authSuccess) {
log.info("刷卡鉴权成功: pileCode={}, gunNo={}, cardNo={}, tradeNo={}",
pileCode, gunNo, cardNo, tradeNo);
} else {
log.warn("刷卡鉴权失败: pileCode={}, gunNo={}, cardNo={}, reason={}",
pileCode, gunNo, cardNo, failReason);
}
}
/**
* 处理查询计费模板请求
*/
private void handleQueryPricing(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
log.info("处理查询计费模板请求: pileCode={}", pileCode);
try {
// 根据 pileCode 查询充电桩
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
log.warn("充电桩不存在: pileCode={}", pileCode);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
// 获取关联的计费模板
BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
if (billingTemplateVO == null || billingTemplateVO.getTemplateId() == null) {
log.warn("充电桩未配置计费模板: pileCode={}", pileCode);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
Long billingTemplateId = Long.parseLong(billingTemplateVO.getTemplateId());
// 查询计费模板
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(billingTemplateId);
if (template == null) {
log.warn("计费模板不存在: pileCode={}, billingTemplateId={}", pileCode, billingTemplateId);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
return;
}
// 转换为 JCPP 格式
JcppPricingModel pricingModel = PricingModelConverter.convert(template);
// 发送应答
jcppDownlinkService.sendQueryPricingAck(pileCode, billingTemplateId, pricingModel);
log.info("发送计费模板查询应答: pileCode={}, pricingId={}", pileCode, billingTemplateId);
} catch (Exception e) {
log.error("处理查询计费模板异常: pileCode=", pileCode, e);
jcppDownlinkService.sendQueryPricingAck(pileCode, null, null);
}
}
/**
* 处理校验计费模板请求
*/
private void handleVerifyPricing(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
Long pricingId = data.getLong("pricingId");
log.info("处理校验计费模板请求: pileCode={}, pricingId={}", pileCode, pricingId);
try {
boolean success = false;
if (pricingId != null) {
PileBillingTemplate template = pileBillingTemplateService.selectPileBillingTemplateById(pricingId);
success = template != null;
}
// 发送应答
jcppDownlinkService.sendVerifyPricingAck(pileCode, success, pricingId);
log.info("发送计费模板校验应答: pileCode={}, pricingId={}, success={}", pileCode, pricingId, success);
} catch (Exception e) {
log.error("处理校验计费模板异常: pileCode={}, pricingId={}", pileCode, pricingId, e);
jcppDownlinkService.sendVerifyPricingAck(pileCode, false, pricingId);
}
}
/**
* 处理会话关闭事件
*/
private void handleSessionClose(JcppUplinkMessage uplinkMessage) {
String pileCode = uplinkMessage.getPileCode();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
String reason = data.getString("reason");
log.info("处理会话关闭: pileCode={}, reason={}", pileCode, reason);
// 更新所有枪状态为离线
int result = pileConnectorInfoService.updateConnectorStatusByPileSn(pileCode, "0");
log.info("更新枪状态为离线: pileCode={}, affectedRows={}", pileCode, result);
// TODO: 查询是否有正在充电的订单,如果有则标记为异常
}
/**
* 处理远程操作结果
*/
@Transactional(rollbackFor = Exception.class)
private void handleRemoteResult(JcppUplinkMessage uplinkMessage) {
String messageType = uplinkMessage.getMessageType();
JSONObject data = JSON.parseObject(uplinkMessage.getData());
if (JcppConstants.MessageType.REMOTE_START_RESULT.equals(messageType)) {
handleRemoteStartResult(data);
} else if (JcppConstants.MessageType.REMOTE_STOP_RESULT.equals(messageType)) {
handleRemoteStopResult(data);
}
}
/**
* 处理远程启动结果
*/
private void handleRemoteStartResult(JSONObject data) {
String tradeNo = data.getString("tradeNo");
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
// 从 data 中获取 pileCode 和 gunNo如果有
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
if (tradeNo == null || tradeNo.isEmpty()) {
log.warn("远程启动结果缺少 tradeNo");
return;
}
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在: tradeNo={}", tradeNo);
return;
}
if (Boolean.TRUE.equals(success)) {
// 启动成功
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("1"); // 充电中
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
String pileConnectorCode = pileCode + gunNo;
pileConnectorInfoService.updateConnectorStatus(pileConnectorCode, "3");
log.info("远程启动成功: tradeNo={}", tradeNo);
} else {
// 启动失败
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("3"); // 已取消
updateOrder.setReason("启动失败: " + failReason);
orderBasicInfoService.updateOrderBasicInfo(updateOrder);
log.warn("远程启动失败: tradeNo={}, reason={}", tradeNo, failReason);
// TODO: 如果已预付费,触发退款流程
}
}
/**
* 处理远程停止结果
*/
private void handleRemoteStopResult(JSONObject data) {
Boolean success = data.getBoolean("success");
String failReason = data.getString("failReason");
// 从 data 中获取 pileCode 和 gunNo如果有
String pileCode = data.getString("pileCode");
String gunNo = data.getString("gunNo");
if (Boolean.TRUE.equals(success)) {
log.info("远程停止成功: pileCode={}, gunNo={}", pileCode, gunNo);
} else {
log.warn("远程停止失败: pileCode=, gunNo={}, reason={}", pileCode, gunNo, failReason);
}
}
/**
* 映射枪状态JCPP 状态 -> 系统状态
*/
private String mapGunStatus(String jcppStatus) {
if (jcppStatus == null) {
return "0";
}
switch (jcppStatus) {
case "IDLE":
return "1"; // 空闲
case "INSERTED":
return "2"; // 占用(未充电)
case "CHARGING":
return "3"; // 占用(充电中)
case "CHARGE_COMPLETE":
return "2"; // 占用(未充电)- 充电完成但未拔枪
case "FAULT":
return "255"; // 故障
case "UNKNOWN":
default:
return "0"; // 离网
}
}
}

View File

@@ -0,0 +1,627 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.jsowell.common.util.id.IdUtils;
import com.jsowell.pile.domain.*;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.*;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.service.IJcppMessageService;
import com.jsowell.pile.jcpp.util.PricingModelConverter;
import com.jsowell.pile.service.*;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* JCPP 消息处理服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppMessageServiceImpl implements IJcppMessageService {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private PileAuthCardService pileAuthCardService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public JcppUplinkResponse handleMessage(JcppUplinkMessage message) {
if (message == null || message.getMessageType() == null) {
log.warn("收到无效的 JCPP 消息: {}", message);
return JcppUplinkResponse.error("无效的消息");
}
String messageType = message.getMessageType();
log.info("收到 JCPP 上行消息, messageId: {}, messageType: {}, pileCode: {}",
message.getMessageId(), messageType, message.getPileCode());
try {
switch (messageType) {
case JcppConstants.MessageType.LOGIN:
return handleLogin(message);
case JcppConstants.MessageType.HEARTBEAT:
return handleHeartbeat(message);
case JcppConstants.MessageType.START_CHARGE:
return handleStartCharge(message);
case JcppConstants.MessageType.REAL_TIME_DATA:
return handleRealTimeData(message);
case JcppConstants.MessageType.TRANSACTION_RECORD:
return handleTransactionRecord(message);
case JcppConstants.MessageType.GUN_STATUS:
return handleGunStatus(message);
case JcppConstants.MessageType.VERIFY_PRICING:
return handleVerifyPricing(message);
case JcppConstants.MessageType.QUERY_PRICING:
return handleQueryPricing(message);
case JcppConstants.MessageType.REMOTE_START_RESULT:
return handleRemoteStartResult(message);
case JcppConstants.MessageType.REMOTE_STOP_RESULT:
return handleRemoteStopResult(message);
default:
log.warn("未知的消息类型: {}", messageType);
return JcppUplinkResponse.error(message.getMessageId(), "未知的消息类型: " + messageType);
}
} catch (Exception e) {
log.error("处理 JCPP 消息异常, messageId: {}, messageType: {}, error: {}",
message.getMessageId(), messageType, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理消息异常: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleLogin(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理登录消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析登录数据
JcppLoginData loginData = parseData(message.getData(), JcppLoginData.class);
if (loginData == null) {
log.warn("登录消息数据解析失败, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "登录数据解析失败");
}
// 查询充电桩是否存在
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
log.warn("充电桩不存在, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩不存在");
}
// 检查充电桩是否被删除
if ("1".equals(pileInfo.getDelFlag())) {
log.warn("充电桩已被删除, pileCode: {}", pileCode);
jcppDownlinkService.loginAck(sessionId, pileCode, false);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩已被删除");
}
// 保存会话信息到 Redis
JcppSessionInfo sessionInfo = JcppSessionInfo.builder()
.sessionId(sessionId)
.pileCode(pileCode)
.remoteAddress(loginData.getRemoteAddress())
.nodeId(loginData.getNodeId())
.nodeHostAddress(loginData.getNodeHostAddress())
.nodeRestPort(loginData.getNodeRestPort())
.nodeGrpcPort(loginData.getNodeGrpcPort())
.protocolName(message.getProtocolName())
.loginTimestamp(System.currentTimeMillis())
.lastActiveTimestamp(System.currentTimeMillis())
.online(true)
.build();
// 保存会话到 Redis
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
stringRedisTemplate.opsForValue().set(sessionKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 保存节点信息到 Redis
String nodeKey = JcppConstants.REDIS_KEY_NODE + pileCode;
stringRedisTemplate.opsForValue().set(nodeKey, JSON.toJSONString(loginData),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 设置在线状态
String onlineKey = JcppConstants.REDIS_KEY_ONLINE + pileCode;
stringRedisTemplate.opsForValue().set(onlineKey, "1",
JcppConstants.ONLINE_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.info("充电桩登录成功, pileCode: {}, sessionId: {}, remoteAddress: {}",
pileCode, sessionId, loginData.getRemoteAddress());
// 发送登录应答
jcppDownlinkService.loginAck(sessionId, pileCode, true);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
public JcppUplinkResponse handleHeartbeat(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.debug("处理心跳消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 更新会话过期时间
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(sessionKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
sessionInfo.setLastActiveTimestamp(System.currentTimeMillis());
stringRedisTemplate.opsForValue().set(sessionKey, JSON.toJSONString(sessionInfo),
JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
// 更新节点信息过期时间
String nodeKey = JcppConstants.REDIS_KEY_NODE + pileCode;
stringRedisTemplate.expire(nodeKey, JcppConstants.SESSION_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 更新在线状态过期时间
String onlineKey = JcppConstants.REDIS_KEY_ONLINE + pileCode;
stringRedisTemplate.opsForValue().set(onlineKey, "1",
JcppConstants.ONLINE_EXPIRE_SECONDS, TimeUnit.SECONDS);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleStartCharge(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理启动充电消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析启动充电数据
JcppStartChargeData startData = parseData(message.getData(), JcppStartChargeData.class);
if (startData == null) {
log.warn("启动充电数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "启动充电数据解析失败");
}
String gunNo = startData.getGunNo();
String cardNo = startData.getCardNo();
String startType = startData.getStartType();
try {
// 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.PILE_DISABLED);
return JcppUplinkResponse.error(message.getMessageId(), "充电桩不存在");
}
// 根据启动类型进行鉴权
String memberId = null;
String merchantId = String.valueOf(pileInfo.getMerchantId());
BigDecimal balance = BigDecimal.ZERO;
if (JcppConstants.StartType.CARD.equals(startType)) {
// 刷卡启动 - 根据逻辑卡号查询授权卡
PileAuthCard authCard = pileAuthCardService.selectCardInfoByLogicCard(cardNo);
if (authCard == null) {
log.warn("授权卡不存在, cardNo: {}", cardNo);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_NOT_EXISTS);
return JcppUplinkResponse.error(message.getMessageId(), "授权卡不存在");
}
// 检查卡状态
if (!"1".equals(authCard.getStatus())) {
log.warn("授权卡已停用, cardNo: {}", cardNo);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_FROZEN);
return JcppUplinkResponse.error(message.getMessageId(), "授权卡已停用");
}
memberId = authCard.getMemberId();
} else if (JcppConstants.StartType.VIN.equals(startType)) {
// VIN码启动
String vinCode = startData.getCarVinCode();
// TODO: 根据VIN码查询会员信息
log.info("VIN码启动, vinCode: ", vinCode);
}
// 查询会员信息和余额
if (memberId != null) {
MemberBasicInfo memberInfo = memberBasicInfoService.selectInfoByMemberId(memberId);
if (memberInfo == null) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_NOT_EXISTS);
return JcppUplinkResponse.error(message.getMessageId(), "会员不存在");
}
// 检查会员状态
if ("1".equals(memberInfo.getStatus())) {
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.ACCOUNT_FROZEN);
return JcppUplinkResponse.error(message.getMessageId(), "会员账户已冻结");
}
// 查询钱包余额
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, merchantId);
if (walletInfo != null) {
BigDecimal principalBalance = walletInfo.getPrincipalBalance() != null ? walletInfo.getPrincipalBalance() : BigDecimal.ZERO;
BigDecimal giftBalance = walletInfo.getGiftBalance() != null ? walletInfo.getGiftBalance() : BigDecimal.ZERO;
balance = principalBalance.add(giftBalance);
}
// 检查余额是否充足最低1元
if (balance.compareTo(BigDecimal.ONE) < 0) {
log.warn("余额不足, memberId: {}, balance: {}", memberId, balance);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.INSUFFICIENT_BALANCE);
return JcppUplinkResponse.error(message.getMessageId(), "余额不足");
}
}
// 生成交易流水号
String tradeNo = IdUtils.fastSimpleUUID();
// 创建订单
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setTransactionCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + "-" + gunNo);
order.setMemberId(memberId);
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setOrderStatus("1"); // 充电中
order.setPayStatus("0"); // 待支付
order.setChargeStartTime(new Date());
order.setCreateTime(new Date());
orderBasicInfoService.insert(order);
// 保存交易流水号与充电桩的映射关系到 Redis
String tradeKey = "jcpp:trade:" + tradeNo;
stringRedisTemplate.opsForValue().set(tradeKey, pileCode, 24, TimeUnit.HOURS);
// 发送启动充电应答
sendStartChargeAck(sessionId, pileCode, gunNo, tradeNo, cardNo, balance.toString(), true, null);
log.info("刷卡启动充电鉴权成功, pileCode: {}, gunNo: {}, tradeNo: {}, memberId: {}, balance: {}",
pileCode, gunNo, tradeNo, memberId, balance);
return JcppUplinkResponse.success(message.getMessageId(), tradeNo);
} catch (Exception e) {
log.error("处理启动充电消息异常, pileCode: {}, error: {}", pileCode, e.getMessage(), e);
sendStartChargeAck(sessionId, pileCode, gunNo, null, cardNo, null,
false, JcppConstants.AuthFailReason.SYSTEM_ERROR);
return JcppUplinkResponse.error(message.getMessageId(), "处理启动充电异常: " + e.getMessage());
}
}
/**
* 发送启动充电应答
*/
private void sendStartChargeAck(String sessionId, String pileCode, String gunNo,
String tradeNo, String cardNo, String limitYuan,
boolean success, String failReason) {
try {
jcppDownlinkService.startChargeAck(sessionId, pileCode, gunNo, tradeNo, cardNo, limitYuan, success, failReason);
} catch (Exception e) {
log.error("发送启动充电应答失败, pileCode: {}, error: {}", pileCode, e.getMessage());
}
}
@Override
public JcppUplinkResponse handleRealTimeData(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.debug("处理实时数据消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// 解析实时数据
JcppRealTimeData realTimeData = parseData(message.getData(), JcppRealTimeData.class);
if (realTimeData == null) {
log.warn("实时数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "实时数据解析失败");
}
String tradeNo = realTimeData.getTradeNo();
String gunNo = realTimeData.getGunNo();
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
// 更新订单实时数据
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
if (realTimeData.getTotalChargingEnergyKWh() != null) {
// updateOrder.setTotalElectricity(new BigDecimal(realTimeData.getTotalChargingEnergyKWh()));
}
if (realTimeData.getTotalChargingCostYuan() != null) {
// updateOrder.setTotalAmount(new BigDecimal(realTimeData.getTotalChargingCostYuan()));
}
if (realTimeData.getTotalChargingDurationMin() != null) {
// updateOrder.setTotalTime(realTimeData.getTotalChargingDurationMin());
}
updateOrder.setUpdateTime(new Date());
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
// 保存实时数据到 Redis用于实时查询
String realtimeKey = "jcpp:realtime:" + tradeNo;
stringRedisTemplate.opsForValue().set(realtimeKey, JSON.toJSONString(realTimeData), 10, TimeUnit.MINUTES);
// 保存监控数据到数据库(可选,根据需要控制写入频率)
// saveMonitorData(order, realTimeData);
log.debug("实时数据处理成功, tradeNo: {}, energy: {}kWh, cost: {}元",
tradeNo, realTimeData.getTotalChargingEnergyKWh(), realTimeData.getTotalChargingCostYuan());
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理实时数据异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理实时数据异常: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleTransactionRecord(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理交易记录消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析交易记录数据
JcppTransactionData transactionData = parseData(message.getData(), JcppTransactionData.class);
if (transactionData == null) {
log.warn("交易记录数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "交易记录数据解析失败");
}
String tradeNo = transactionData.getTradeNo();
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
// 仍然发送确认,避免充电桩重复上报
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
// 幂等性检查:如果订单已经是完成状态,直接返回成功
if ("2".equals(order.getOrderStatus())) {
log.info("订单已完成,忽略重复的交易记录, tradeNo: {}", tradeNo);
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
return JcppUplinkResponse.success(message.getMessageId(), null);
}
// 更新订单信息
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setOrderStatus("2"); // 充电完成
updateOrder.setChargeEndTime(transactionData.getEndTs() != null ? new Date(transactionData.getEndTs()) : new Date());
updateOrder.setReason(transactionData.getStopReason());
// 设置电量
if (transactionData.getTotalEnergyKWh() != null) {
// updateOrder.setTotalElectricity(new BigDecimal(transactionData.getTotalEnergyKWh()));
}
// 设置金额(如果充电桩上报了金额则使用,否则需要根据计费模板计算)
if (transactionData.getTotalAmountYuan() != null) {
// updateOrder.setTotalAmount(new BigDecimal(transactionData.getTotalAmountYuan()));
}
// 计算充电时长
if (transactionData.getStartTs() != null && transactionData.getEndTs() != null) {
long durationMin = (transactionData.getEndTs() - transactionData.getStartTs()) / 60000;
// updateOrder.setTotalTime((int) durationMin);
}
updateOrder.setUpdateTime(new Date());
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
// 更新枪状态为空闲
String connectorCode = pileCode + "-" + transactionData.getGunNo();
// pileConnectorInfoService.updateConnectorStatus(connectorCode, "0"); // 空闲
// 发送交易记录确认
jcppDownlinkService.transactionRecordAck(sessionId, tradeNo, true);
// 清理 Redis 中的实时数据
String realtimeKey = "jcpp:realtime:" + tradeNo;
stringRedisTemplate.delete(realtimeKey);
log.info("交易记录处理成功, tradeNo: {}, energy: {}kWh, amount: {}元, stopReason: {}",
tradeNo, transactionData.getTotalEnergyKWh(),
transactionData.getTotalAmountYuan(), transactionData.getStopReason());
// TODO: 触发结算流程(如果是预付费模式)
// orderBasicInfoService.realTimeOrderSplit(order);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理交易记录异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理交易记录异常: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleGunStatus(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.info("处理枪状态消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// TODO: 解析枪状态数据并更新数据库
// 枪状态变化通常用于更新充电枪的实时状态
return JcppUplinkResponse.success(message.getMessageId(), null);
}
@Override
public JcppUplinkResponse handleVerifyPricing(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理校验计费消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 校验计费模板充电桩上报当前使用的计费模板ID平台校验是否一致
// 如果不一致,需要重新下发计费模板
try {
// 获取平台当前的计费模板
BillingTemplateVO billingTemplate = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
Long currentPricingId = billingTemplate != null ? Long.parseLong(billingTemplate.getTemplateId()) : null;
// TODO: 从消息中获取充电桩上报的计费模板ID进行比对
// 这里简化处理,直接返回成功
jcppDownlinkService.verifyPricingAck(sessionId, pileCode, true, currentPricingId);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("校验计费模板失败, pileCode: , error: {}", pileCode, e.getMessage(), e);
jcppDownlinkService.verifyPricingAck(sessionId, pileCode, false, null);
return JcppUplinkResponse.error(message.getMessageId(), "校验计费模板失败: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleQueryPricing(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理查询计费消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
try {
// 根据充电桩编码查询计费模板
BillingTemplateVO billingTemplate = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileCode);
if (billingTemplate == null) {
log.warn("未找到充电桩的计费模板, pileCode: {}", pileCode);
jcppDownlinkService.queryPricingAck(sessionId, pileCode, null, null);
return JcppUplinkResponse.error(message.getMessageId(), "未找到计费模板");
}
// 转换为 JCPP 计费模板格式
JcppPricingModel pricingModel = PricingModelConverter.convert(billingTemplate);
// 发送计费模板应答
jcppDownlinkService.queryPricingAck(sessionId, pileCode, Long.parseLong(billingTemplate.getTemplateId()), pricingModel);
log.info("查询计费模板成功, pileCode: {}, templateId: {}, templateName: {}",
pileCode, billingTemplate.getTemplateId(), billingTemplate.getTemplateName());
return JcppUplinkResponse.success(message.getMessageId(), pricingModel);
} catch (Exception e) {
log.error("查询计费模板失败, pileCode: {}, error: {}", pileCode, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "查询计费模板失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public JcppUplinkResponse handleRemoteStartResult(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
String sessionId = message.getSessionId();
log.info("处理远程启动结果消息, pileCode: {}, sessionId: {}", pileCode, sessionId);
// 解析远程启动结果数据
JcppRemoteStartResultData resultData = parseData(message.getData(), JcppRemoteStartResultData.class);
if (resultData == null) {
log.warn("远程启动结果数据解析失败, pileCode: {}", pileCode);
return JcppUplinkResponse.error(message.getMessageId(), "远程启动结果数据解析失败");
}
String tradeNo = resultData.getTradeNo();
boolean success = Boolean.TRUE.equals(resultData.getSuccess());
try {
// 根据交易流水号查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByTransactionCode(tradeNo);
if (order == null) {
log.warn("订单不存在, tradeNo: {}", tradeNo);
return JcppUplinkResponse.error(message.getMessageId(), "订单不存在");
}
OrderBasicInfo updateOrder = new OrderBasicInfo();
updateOrder.setId(order.getId());
updateOrder.setUpdateTime(new Date());
if (success) {
// 启动成功
updateOrder.setOrderStatus("1"); // 充电中
log.info("远程启动充电成功, tradeNo: {}, pileCode: {}, gunNo: {}",
tradeNo, pileCode, resultData.getGunNo());
} else {
// 启动失败
updateOrder.setOrderStatus("4"); // 异常结束
updateOrder.setReason(resultData.getFailReason());
log.warn("远程启动充电失败, tradeNo: {}, pileCode: {}, failReason: {}",
tradeNo, pileCode, resultData.getFailReason());
// TODO: 触发退款流程(如果已预付)
// refundService.refund(order);
}
orderBasicInfoService.updateByPrimaryKeySelective(updateOrder);
return JcppUplinkResponse.success(message.getMessageId(), null);
} catch (Exception e) {
log.error("处理远程启动结果异常, tradeNo: {}, error: {}", tradeNo, e.getMessage(), e);
return JcppUplinkResponse.error(message.getMessageId(), "处理远程启动结果异常: " + e.getMessage());
}
}
@Override
public JcppUplinkResponse handleRemoteStopResult(JcppUplinkMessage message) {
String pileCode = message.getPileCode();
log.info("处理远程停止结果消息, pileCode: {}, sessionId: {}", pileCode, message.getSessionId());
// 远程停止结果通常不需要特殊处理,等待交易记录上报即可
// 这里只记录日志
return JcppUplinkResponse.success(message.getMessageId(), null);
}
/**
* 解析消息数据
*/
private <T> T parseData(Object data, Class<T> clazz) {
if (data == null) {
return null;
}
try {
if (data instanceof String) {
return JSON.parseObject((String) data, clazz);
}
return JSON.parseObject(JSON.toJSONString(data), clazz);
} catch (Exception e) {
log.error("解析消息数据失败, data: , clazz: {}, error: {}", data, clazz.getName(), e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,867 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Lists;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.JcppSyncRecord;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.PileConnectorInfo;
import com.jsowell.pile.domain.PileModelInfo;
import com.jsowell.pile.jcpp.dto.sync.*;
import com.jsowell.pile.jcpp.service.IJcppAuthService;
import com.jsowell.pile.jcpp.service.IJcppPileSyncService;
import com.jsowell.pile.mapper.JcppSyncRecordMapper;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileConnectorInfoService;
import com.jsowell.pile.service.PileModelInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* JCPP 充电桩同步服务实现
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppPileSyncServiceImpl implements IJcppPileSyncService {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired
private JcppSyncRecordMapper jcppSyncRecordMapper;
@Autowired
private IJcppAuthService jcppAuthService;
@Autowired
@Qualifier("jcppSyncRestTemplate")
private RestTemplate restTemplate;
@Autowired
private PileModelInfoService pileModelInfoService;
@Value("${jcpp.sync.api-url:http://localhost:8180/api/sync}")
private String jcppApiUrl;
@Value("${jcpp.sync.batch-size:1000}")
private int batchSize;
@Value("${jcpp.sync.timeout:60000}")
private int timeout;
/**
* 全量同步充电桩数据到 JCPP批量处理
*/
@Override
public JcppSyncResponse syncAllPiles() {
log.info("开始全量同步充电桩数据到 JCPP批量大小: {}", batchSize);
// 创建同步记录
JcppSyncRecord record = createSyncRecord("FULL");
try {
// 1. 查询所有充电桩(未删除的)
PileBasicInfo queryPile = new PileBasicInfo();
queryPile.setDelFlag("0");
List<PileBasicInfo> pileList = pileBasicInfoService.selectPileBasicInfoList(queryPile);
log.info("查询到 {} 个充电桩,准备分批同步", pileList.size());
// 2. 查询所有充电枪(未删除的)
PileConnectorInfo queryGun = new PileConnectorInfo();
queryGun.setDelFlag("0");
List<PileConnectorInfo> gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
log.info("查询到 {} 个充电枪", gunList.size());
// 3. 按批次分割充电桩列表
List<List<PileBasicInfo>> pileBatches = Lists.partition(pileList, batchSize);
int totalBatches = pileBatches.size();
log.info("充电桩分为 {} 批,每批最多 {} 台", totalBatches, batchSize);
// 4. 汇总所有批次的同步结果
List<JcppSyncResult> allPileResults = new ArrayList<>();
List<JcppSyncResult> allGunResults = new ArrayList<>();
// 5. 逐批处理充电桩
for (int i = 0; i < pileBatches.size(); i++) {
List<PileBasicInfo> batchPiles = pileBatches.get(i);
int batchNo = i + 1;
long batchStartTime = System.currentTimeMillis();
log.info("开始处理第 {}/{} 批充电桩,本批数量: {}", batchNo, totalBatches, batchPiles.size());
try {
// 5.1 转换充电桩数据
long convertStartTime = System.currentTimeMillis();
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(batchPiles);
long convertTime = System.currentTimeMillis() - convertStartTime;
log.debug("第 {} 批:充电桩数据转换耗时 {} ms", batchNo, convertTime);
// 5.2 先同步充电桩(充电枪依赖充电桩,必须先同步充电桩)
log.info("第 {} 批:开始同步充电桩...", batchNo);
long pileSyncStartTime = System.currentTimeMillis();
List<JcppSyncResult> batchPileResults = syncPilesToJcpp(pileDTOs);
long pileSyncTime = System.currentTimeMillis() - pileSyncStartTime;
allPileResults.addAll(batchPileResults);
// 统计充电桩同步结果
long batchPileSuccess = batchPileResults.stream().filter(JcppSyncResult::isSuccess).count();
long batchPileFailed = batchPiles.size() - batchPileSuccess;
log.info("第 {} 批:充电桩同步完成 {}/{} (成功/总数),耗时 {} ms",
batchNo, batchPileSuccess, batchPiles.size(), pileSyncTime);
// 5.3 查找本批充电桩对应的充电枪(只同步充电桩成功的枪)
long gunFilterStartTime = System.currentTimeMillis();
List<String> successPileSns = batchPileResults.stream()
.filter(JcppSyncResult::isSuccess)
.map(JcppSyncResult::getCode)
.collect(java.util.stream.Collectors.toList());
List<PileConnectorInfo> batchGuns = gunList.stream()
.filter(gun -> successPileSns.contains(gun.getPileSn()))
.collect(java.util.stream.Collectors.toList());
long gunFilterTime = System.currentTimeMillis() - gunFilterStartTime;
log.info("第 {} 批:找到 {} 个充电枪(对应 {} 个成功的充电桩),筛选耗时 {} ms",
batchNo, batchGuns.size(), successPileSns.size(), gunFilterTime);
// 5.4 同步充电枪(只同步充电桩成功的枪)
List<JcppSyncResult> batchGunResults = new ArrayList<>();
if (!batchGuns.isEmpty()) {
long gunConvertStartTime = System.currentTimeMillis();
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(batchGuns);
long gunConvertTime = System.currentTimeMillis() - gunConvertStartTime;
log.debug("第 {} 批:充电枪数据转换耗时 {} ms", batchNo, gunConvertTime);
log.info("第 {} 批:开始同步充电枪...", batchNo);
long gunSyncStartTime = System.currentTimeMillis();
batchGunResults = syncGunsToJcpp(gunDTOs);
long gunSyncTime = System.currentTimeMillis() - gunSyncStartTime;
allGunResults.addAll(batchGunResults);
long batchGunSuccess = batchGunResults.stream().filter(JcppSyncResult::isSuccess).count();
log.info("第 {} 批:充电枪同步完成 {}/{} (成功/总数),耗时 {} ms",
batchNo, batchGunSuccess, batchGuns.size(), gunSyncTime);
} else {
log.warn("第 {} 批:没有需要同步的充电枪(充电桩全部失败)", batchNo);
}
// 5.5 统计本批总体结果和耗时
long batchGunSuccess = batchGunResults.stream().filter(JcppSyncResult::isSuccess).count();
long batchTotalTime = System.currentTimeMillis() - batchStartTime;
log.info("第 {}/{} 批同步完成: 充电桩 {}/{}, 充电枪 {}/{}, 总耗时 {} ms",
batchNo, totalBatches,
batchPileSuccess, batchPiles.size(),
batchGunSuccess, batchGuns.size(),
batchTotalTime);
} catch (Exception e) {
long batchTotalTime = System.currentTimeMillis() - batchStartTime;
log.error("第 {} 批同步失败,耗时 {} ms", batchNo, batchTotalTime, e);
// 记录失败,但继续处理下一批
for (PileBasicInfo pile : batchPiles) {
allPileResults.add(JcppSyncResult.fail(pile.getSn(), "批次同步异常: " + e.getMessage()));
}
}
}
// 6. 构建最终响应
JcppSyncResponse response = JcppSyncResponse.build(allPileResults, allGunResults);
// 7. 更新同步记录
updateSyncRecord(record, response, "SUCCESS");
log.info("全量同步完成: 充电桩 {}/{}, 充电枪 {}/{}",
response.getSuccessPiles(), response.getTotalPiles(),
response.getSuccessGuns(), response.getTotalGuns());
return response;
} catch (Exception e) {
log.error("全量同步失败", e);
updateSyncRecord(record, null, "FAILED", e.getMessage());
throw new RuntimeException("全量同步失败: " + e.getMessage(), e);
}
}
/**
* 增量同步充电桩数据到 JCPP
*/
@Override
public JcppSyncResponse syncIncrementalPiles(Date lastSyncTime) {
log.info("开始增量同步充电桩数据到 JCPP");
// 如果未指定上次同步时间,查询最后一次成功的同步记录
if (lastSyncTime == null) {
JcppSyncRecord lastRecord = jcppSyncRecordMapper.selectLastSuccessRecord("INCREMENTAL");
if (lastRecord != null) {
lastSyncTime = lastRecord.getStartTime();
log.info("使用最后一次成功同步时间: {}", lastSyncTime);
} else {
// 如果没有历史记录,使用全量同步
log.warn("未找到历史同步记录,改为全量同步");
return syncAllPiles();
}
}
// 创建同步记录
JcppSyncRecord record = createSyncRecord("INCREMENTAL");
try {
// 1. 查询更新时间大于 lastSyncTime 的充电桩
// 注意:这里暂时使用全量查询,然后在内存中过滤
// TODO: 后续可以在 Mapper 中添加按 updateTime 查询的方法以提升性能
PileBasicInfo queryPile = new PileBasicInfo();
queryPile.setDelFlag("0");
List<PileBasicInfo> allPiles = pileBasicInfoService.selectPileBasicInfoList(queryPile);
// 过滤出更新时间大于 lastSyncTime 的充电桩
final Date finalLastSyncTime = lastSyncTime;
List<PileBasicInfo> pileList = allPiles.stream()
.filter(pile -> pile.getUpdateTime() != null && pile.getUpdateTime().after(finalLastSyncTime))
.collect(java.util.stream.Collectors.toList());
log.info("查询到 {} 个更新的充电桩", pileList.size());
// 2. 查询更新时间大于 lastSyncTime 的充电枪
PileConnectorInfo queryGun = new PileConnectorInfo();
queryGun.setDelFlag("0");
List<PileConnectorInfo> allGuns = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
// 过滤出更新时间大于 lastSyncTime 的充电枪
List<PileConnectorInfo> gunList = allGuns.stream()
.filter(gun -> gun.getUpdateTime() != null && gun.getUpdateTime().after(finalLastSyncTime))
.collect(java.util.stream.Collectors.toList());
log.info("查询到 {} 个更新的充电枪", gunList.size());
// 3. 转换数据格式
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(pileList);
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
// 4. 调用 JCPP 同步接口
JcppSyncResponse response = callJcppSyncApi(pileDTOs, gunDTOs);
// 5. 更新同步记录
updateSyncRecord(record, response, "SUCCESS");
log.info("增量同步完成: 充电桩 {}/{}, 充电枪 {}/{}",
response.getSuccessPiles(), response.getTotalPiles(),
response.getSuccessGuns(), response.getTotalGuns());
return response;
} catch (Exception e) {
log.error("增量同步失败", e);
updateSyncRecord(record, null, "FAILED", e.getMessage());
throw new RuntimeException("增量同步失败: " + e.getMessage(), e);
}
}
/**
* 同步单个充电桩
*/
@Override
public boolean syncSinglePile(String pileSn) {
log.info("开始同步单个充电桩: {}", pileSn);
try {
// 1. 查询充电桩
PileBasicInfo pile = pileBasicInfoService.selectPileBasicInfoBySN(pileSn);
if (pile == null) {
log.warn("充电桩不存在: {}", pileSn);
return false;
}
// 2. 查询该充电桩的所有充电枪
PileConnectorInfo queryGun = new PileConnectorInfo();
queryGun.setPileSn(pileSn);
queryGun.setDelFlag("0");
List<PileConnectorInfo> gunList = pileConnectorInfoService.selectPileConnectorInfoList(queryGun);
// 3. 转换数据格式
List<JcppPileSyncDTO> pileDTOs = convertPilesToDTO(Lists.newArrayList(pile));
List<JcppGunSyncDTO> gunDTOs = convertGunsToDTO(gunList);
// 4. 调用 JCPP 同步接口
JcppSyncResponse response = callJcppSyncApi(pileDTOs, gunDTOs);
log.info("单个充电桩同步完成: {}, 结果: {}", pileSn, response.getSuccess());
return response.getSuccess();
} catch (Exception e) {
log.error("同步单个充电桩失败: {}", pileSn, e);
return false;
}
}
/**
* 转换充电桩数据为 DTO
*/
private List<JcppPileSyncDTO> convertPilesToDTO(List<PileBasicInfo> pileList) {
List<JcppPileSyncDTO> dtoList = new ArrayList<>();
for (PileBasicInfo pile : pileList) {
JcppPileSyncDTO dto = new JcppPileSyncDTO();
// 基本字段
dto.setPileCode(pile.getSn());
String pileName = pile.getName();
// 如果名称为空,使用充电桩编号
if (StringUtils.isBlank(pileName)) {
pileName = pile.getSn();
}
dto.setPileName(pileName);
dto.setProtocol("yunkuaichongV150");
// 品牌、型号、制造商(可为空)
dto.setBrand("jsowell"); // Web 项目中没有这些字段
dto.setModel(null);
dto.setManufacturer("jsowell");
// 类型映射:从 pile_model_info 表获取 speed_type
// 1-快充(DC直流) → DC, 2-慢充(AC交流) → AC
String type = "AC"; // 默认交流桩
if (pile.getModelId() != null) {
try {
PileModelInfo modelInfo = pileModelInfoService.selectPileModelInfoById(pile.getModelId());
if (modelInfo != null && StringUtils.isNotEmpty(modelInfo.getSpeedType())) {
if ("1".equals(modelInfo.getSpeedType())) {
type = "DC"; // 快充-直流
} else if ("2".equals(modelInfo.getSpeedType())) {
type = "AC"; // 慢充-交流
}
}
} catch (Exception e) {
log.warn("查询充电桩型号信息失败使用默认类型AC: pileSn={}, modelId={}", pile.getSn(), pile.getModelId(), e);
}
}
dto.setType(type);
// 构建附加信息
JSONObject additionalInfo = new JSONObject();
additionalInfo.put("webPileId", pile.getId());
additionalInfo.put("webStationId", pile.getStationId());
additionalInfo.put("businessType", pile.getBusinessType());
additionalInfo.put("secretKey", pile.getSecretKey());
// additionalInfo.put("longitude", pile.getLongitude());
// additionalInfo.put("latitude", pile.getLatitude());
additionalInfo.put("iccid", pile.getIccId());
additionalInfo.put("merchantId", pile.getMerchantId());
// additionalInfo.put("vinFlag", pile.get());
dto.setAdditionalInfo(additionalInfo);
dtoList.add(dto);
}
return dtoList;
}
/**
* 转换充电枪数据为 DTO
*/
private List<JcppGunSyncDTO> convertGunsToDTO(List<PileConnectorInfo> gunList) {
List<JcppGunSyncDTO> dtoList = new ArrayList<>();
for (PileConnectorInfo gun : gunList) {
JcppGunSyncDTO dto = new JcppGunSyncDTO();
// 基本字段
dto.setGunCode(gun.getPileConnectorCode());
// 充电枪名称:如果为空,使用枪号生成默认名称
String gunName = gun.getName();
if (StringUtils.isEmpty(gunName)) {
String gunNo = extractGunNo(gun.getPileConnectorCode());
gunName = gun.getPileSn() + "" + gunNo + "号枪";
// log.warn("充电枪名称为空,使用默认名称: {} (gunCode: {})", gunName, gun.getPileConnectorCode());
}
dto.setGunName(gunName);
dto.setPileCode(gun.getPileSn());
// 提取枪号(最后 2 位)
String gunNo = extractGunNo(gun.getPileConnectorCode());
dto.setGunNo(gunNo);
// 构建附加信息
JSONObject additionalInfo = new JSONObject();
additionalInfo.put("webGunId", gun.getId());
additionalInfo.put("status", gun.getStatus());
additionalInfo.put("parkNo", gun.getParkNo());
dto.setAdditionalInfo(additionalInfo);
dtoList.add(dto);
}
return dtoList;
}
/**
* 从充电枪编码中提取枪号(最后 2 位)
*/
private String extractGunNo(String gunCode) {
if (StringUtils.isEmpty(gunCode) || gunCode.length() < 2) {
return "01"; // 默认值
}
return gunCode.substring(gunCode.length() - 2);
}
/**
* 调用 JCPP 同步接口
*/
private JcppSyncResponse callJcppSyncApi(List<JcppPileSyncDTO> pileDTOs, List<JcppGunSyncDTO> gunDTOs) {
List<JcppSyncResult> pileResults = new ArrayList<>();
List<JcppSyncResult> gunResults = new ArrayList<>();
try {
// 1. 同步充电桩
if (pileDTOs != null && !pileDTOs.isEmpty()) {
pileResults = syncPilesToJcpp(pileDTOs);
}
// 2. 同步充电枪
if (gunDTOs != null && !gunDTOs.isEmpty()) {
gunResults = syncGunsToJcpp(gunDTOs);
}
// 3. 构建响应
return JcppSyncResponse.build(pileResults, gunResults);
} catch (Exception e) {
log.error("调用 JCPP 同步接口失败", e);
throw new RuntimeException("调用 JCPP 同步接口失败: " + e.getMessage(), e);
}
}
/**
* 同步充电桩到 JCPP
*/
private List<JcppSyncResult> syncPilesToJcpp(List<JcppPileSyncDTO> pileDTOs) {
String url = jcppApiUrl + "/piles";
long methodStartTime = System.currentTimeMillis();
try {
// 获取访问令牌
long tokenStartTime = System.currentTimeMillis();
String token = jcppAuthService.getAccessToken();
long tokenTime = System.currentTimeMillis() - tokenStartTime;
log.info("【性能】获取访问令牌耗时: {} ms", tokenTime);
if (token == null || token.isEmpty()) {
log.error("无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
long buildStartTime = System.currentTimeMillis();
JSONObject requestBody = new JSONObject();
requestBody.put("piles", pileDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
long buildTime = System.currentTimeMillis() - buildStartTime;
log.info("【性能】构建充电桩请求体耗时: {} ms", buildTime);
// 发送请求
log.info("调用 JCPP 充电桩同步接口: {}, 数量: {}", url, pileDTOs.size());
long httpStartTime = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
long httpTime = System.currentTimeMillis() - httpStartTime;
log.info("【性能】JCPP 充电桩接口 HTTP 请求耗时: {} ms (网络+JCPP处理)", httpTime);
// 打印响应状态和内容
// log.info("JCPP 充电桩同步接口响应 - 状态码: {}, 响应体: {}", response.getStatusCode(), response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
// 解析响应
long parseStartTime = System.currentTimeMillis();
JSONObject responseBody = JSON.parseObject(response.getBody());
// JCPP 响应格式:{ "success": true, "data": { "results": [...] } }
// 需要从 data 中获取 results
JSONObject data = responseBody.getJSONObject("data");
List<JcppSyncResult> results = null;
if (data != null) {
results = data.getList("results", JcppSyncResult.class);
} else {
log.warn("JCPP 充电桩同步响应中没有 data 字段");
results = new ArrayList<>();
}
long parseTime = System.currentTimeMillis() - parseStartTime;
log.info("【性能】解析充电桩响应耗时: {} ms", parseTime);
// 统计结果
long successCount = results != null ? results.stream().filter(JcppSyncResult::isSuccess).count() : 0;
long failCount = results != null ? results.size() - successCount : 0;
long totalTime = System.currentTimeMillis() - methodStartTime;
log.info("JCPP 充电桩同步结果 - 成功: {}, 失败: {}, 方法总耗时: {} ms", successCount, failCount, totalTime);
return results != null ? results : new ArrayList<>();
} else if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// token 过期,清除缓存并重试一次
log.warn("JCPP 访问令牌已过期,清除缓存并重试");
jcppAuthService.clearToken();
// 递归调用重试(只重试一次)
return retrySyncPilesToJcpp(pileDTOs);
} else {
log.error("JCPP 充电桩同步接口返回错误 - 状态码: {}, 响应体: {}",
response.getStatusCode(), response.getBody());
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口返回错误: " + response.getStatusCode()));
}
return results;
}
} catch (Exception e) {
log.error("调用 JCPP 充电桩同步接口异常", e);
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口调用异常: " + e.getMessage()));
}
return results;
}
}
/**
* 重试同步充电桩token 过期时使用)
*/
private List<JcppSyncResult> retrySyncPilesToJcpp(List<JcppPileSyncDTO> pileDTOs) {
String url = jcppApiUrl + "/piles";
try {
// 重新获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("重试时仍无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("piles", pileDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
// 解析响应
JSONObject responseBody = JSON.parseObject(response.getBody());
// JCPP 响应格式:{ "success": true, "data": { "results": [...] } }
JSONObject data = responseBody.getJSONObject("data");
List<JcppSyncResult> results = null;
if (data != null) {
results = data.getList("results", JcppSyncResult.class);
} else {
log.warn("重试JCPP 充电桩同步响应中没有 data 字段");
results = new ArrayList<>();
}
return results != null ? results : new ArrayList<>();
} else {
log.error("JCPP 充电桩同步接口返回错误: {}", response.getStatusCode());
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口返回错误: " + response.getStatusCode()));
}
return results;
}
} catch (Exception e) {
log.error("重试调用 JCPP 充电桩同步接口异常", e);
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppPileSyncDTO dto : pileDTOs) {
results.add(JcppSyncResult.fail(dto.getPileCode(), "接口调用异常: " + e.getMessage()));
}
return results;
}
}
/**
* 同步充电枪到 JCPP
*/
private List<JcppSyncResult> syncGunsToJcpp(List<JcppGunSyncDTO> gunDTOs) {
String url = jcppApiUrl + "/guns";
long methodStartTime = System.currentTimeMillis();
try {
// 获取访问令牌
long tokenStartTime = System.currentTimeMillis();
String token = jcppAuthService.getAccessToken();
long tokenTime = System.currentTimeMillis() - tokenStartTime;
log.info("【性能】获取访问令牌耗时: {} ms", tokenTime);
if (token == null || token.isEmpty()) {
log.error("无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
long buildStartTime = System.currentTimeMillis();
JSONObject requestBody = new JSONObject();
requestBody.put("guns", gunDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
long buildTime = System.currentTimeMillis() - buildStartTime;
log.info("【性能】构建充电枪请求体耗时: {} ms", buildTime);
// 发送请求
log.info("调用 JCPP 充电枪同步接口: {}, 数量: {}", url, gunDTOs.size());
long httpStartTime = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
long httpTime = System.currentTimeMillis() - httpStartTime;
log.info("【性能】JCPP 充电枪接口 HTTP 请求耗时: {} ms (网络+JCPP处理)", httpTime);
// 打印响应状态和内容
// log.info("JCPP 充电枪同步接口响应 - 状态码: {}, 响应体: {}", response.getStatusCode(), response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
// 解析响应
long parseStartTime = System.currentTimeMillis();
JSONObject responseBody = JSON.parseObject(response.getBody());
// JCPP 响应格式:{ "success": true, "data": { "results": [...] } }
// 需要从 data 中获取 results
JSONObject data = responseBody.getJSONObject("data");
List<JcppSyncResult> results = null;
if (data != null) {
results = data.getList("results", JcppSyncResult.class);
} else {
log.warn("JCPP 充电枪同步响应中没有 data 字段");
results = new ArrayList<>();
}
long parseTime = System.currentTimeMillis() - parseStartTime;
log.info("【性能】解析充电枪响应耗时: {} ms", parseTime);
// 统计结果
long successCount = results != null ? results.stream().filter(JcppSyncResult::isSuccess).count() : 0;
long failCount = results != null ? results.size() - successCount : 0;
long totalTime = System.currentTimeMillis() - methodStartTime;
log.info("JCPP 充电枪同步结果 - 成功: {}, 失败: {}, 方法总耗时: {} ms", successCount, failCount, totalTime);
return results != null ? results : new ArrayList<>();
} else if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// token 过期,清除缓存并重试一次
log.warn("JCPP 访问令牌已过期,清除缓存并重试");
jcppAuthService.clearToken();
// 递归调用重试(只重试一次)
return retrySyncGunsToJcpp(gunDTOs);
} else {
log.error("JCPP 充电枪同步接口返回错误 - 状态码: {}, 响应体: {}",
response.getStatusCode(), response.getBody());
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口返回错误: " + response.getStatusCode()));
}
return results;
}
} catch (Exception e) {
log.error("调用 JCPP 充电枪同步接口异常", e);
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口调用异常: " + e.getMessage()));
}
return results;
}
}
/**
* 重试同步充电枪token 过期时使用)
*/
private List<JcppSyncResult> retrySyncGunsToJcpp(List<JcppGunSyncDTO> gunDTOs) {
String url = jcppApiUrl + "/guns";
try {
// 重新获取访问令牌
String token = jcppAuthService.getAccessToken();
if (token == null || token.isEmpty()) {
log.error("重试时仍无法获取 JCPP 访问令牌");
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "无法获取访问令牌"));
}
return results;
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("guns", gunDTOs);
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
// 解析响应
JSONObject responseBody = JSON.parseObject(response.getBody());
// JCPP 响应格式:{ "success": true, "data": { "results": [...] } }
JSONObject data = responseBody.getJSONObject("data");
List<JcppSyncResult> results = null;
if (data != null) {
results = data.getList("results", JcppSyncResult.class);
} else {
log.warn("重试JCPP 充电枪同步响应中没有 data 字段");
results = new ArrayList<>();
}
return results != null ? results : new ArrayList<>();
} else {
log.error("JCPP 充电枪同步接口返回错误: {}", response.getStatusCode());
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口返回错误: " + response.getStatusCode()));
}
return results;
}
} catch (Exception e) {
log.error("重试调用 JCPP 充电枪同步接口异常", e);
// 返回失败结果
List<JcppSyncResult> results = new ArrayList<>();
for (JcppGunSyncDTO dto : gunDTOs) {
results.add(JcppSyncResult.fail(dto.getGunCode(), "接口调用异常: " + e.getMessage()));
}
return results;
}
}
/**
* 创建同步记录
*/
private JcppSyncRecord createSyncRecord(String syncType) {
JcppSyncRecord record = new JcppSyncRecord();
record.setSyncType(syncType);
record.setSyncStatus("RUNNING");
record.setStartTime(new Date());
jcppSyncRecordMapper.insertJcppSyncRecord(record);
return record;
}
/**
* 更新同步记录(成功)
*/
private void updateSyncRecord(JcppSyncRecord record, JcppSyncResponse response, String status) {
updateSyncRecord(record, response, status, null);
}
/**
* 更新同步记录
*/
private void updateSyncRecord(JcppSyncRecord record, JcppSyncResponse response, String status, String errorMessage) {
record.setSyncStatus(status);
record.setEndTime(new Date());
if (response != null) {
record.setTotalPiles(response.getTotalPiles());
record.setSuccessPiles(response.getSuccessPiles());
record.setFailedPiles(response.getFailedPiles());
record.setTotalGuns(response.getTotalGuns());
record.setSuccessGuns(response.getSuccessGuns());
record.setFailedGuns(response.getFailedGuns());
if (response.getErrors() != null && !response.getErrors().isEmpty()) {
// 限制错误信息长度,避免超过 TEXT 字段限制65535字节
// 只保存前100条错误信息并限制总长度不超过60000字符
List<String> errors = response.getErrors();
int maxErrors = Math.min(100, errors.size());
StringBuilder errorBuilder = new StringBuilder();
for (int i = 0; i < maxErrors; i++) {
String error = errors.get(i);
// 如果添加这条错误后会超过限制,则停止
if (errorBuilder.length() + error.length() + 2 > 60000) {
errorBuilder.append("... (还有 ").append(errors.size() - i).append(" 条错误信息被省略)");
break;
}
if (i > 0) {
errorBuilder.append("; ");
}
errorBuilder.append(error);
}
// 如果还有更多错误但没有超长,也添加提示
if (maxErrors < errors.size() && errorBuilder.length() < 60000) {
errorBuilder.append("; ... (还有 ").append(errors.size() - maxErrors).append(" 条错误信息被省略)");
}
record.setErrorMessage(errorBuilder.toString());
}
}
if (errorMessage != null) {
// 限制单个错误消息的长度
if (errorMessage.length() > 60000) {
errorMessage = errorMessage.substring(0, 60000) + "... (错误信息过长已截断)";
}
record.setErrorMessage(errorMessage);
}
jcppSyncRecordMapper.updateJcppSyncRecord(record);
}
}

View File

@@ -0,0 +1,179 @@
package com.jsowell.pile.jcpp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.jsowell.common.util.id.IdUtils;
import com.jsowell.pile.domain.MemberBasicInfo;
import com.jsowell.pile.domain.MemberWalletInfo;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.jcpp.constant.JcppConstants;
import com.jsowell.pile.jcpp.dto.JcppSessionInfo;
import com.jsowell.pile.jcpp.service.IJcppDownlinkService;
import com.jsowell.pile.jcpp.service.IJcppRemoteChargeService;
import com.jsowell.pile.service.MemberBasicInfoService;
import com.jsowell.pile.service.MemberWalletInfoService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileBasicInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* JCPP 远程充电服务实现类
*
* @author jsowell
*/
@Slf4j
@Service
public class JcppRemoteChargeServiceImpl implements IJcppRemoteChargeService {
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private MemberBasicInfoService memberBasicInfoService;
@Autowired
private MemberWalletInfoService memberWalletInfoService;
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private IJcppDownlinkService jcppDownlinkService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public String remoteStartCharging(String memberId, String pileCode, String gunNo, String payAmount) {
log.info("远程启动充电, memberId: {}, pileCode: {}, gunNo: {}, payAmount: {}",
memberId, pileCode, gunNo, payAmount);
// 1. 检查充电桩是否在线(使用新的 Redis Set 方式)
if (!jcppDownlinkService.isPileOnline(pileCode)) {
throw new RuntimeException("充电桩离线,请稍后重试");
}
// 2. 查询充电桩信息
PileBasicInfo pileInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileCode);
if (pileInfo == null) {
throw new RuntimeException("充电桩不存在");
}
// 3. 查询会员信息
MemberBasicInfo memberInfo = memberBasicInfoService.selectInfoByMemberId(memberId);
if (memberInfo == null) {
throw new RuntimeException("会员不存在");
}
// 4. 检查会员状态
if ("1".equals(memberInfo.getStatus())) {
throw new RuntimeException("会员账户已冻结");
}
// 5. 查询钱包余额
String merchantId = String.valueOf(pileInfo.getMerchantId());
MemberWalletInfo walletInfo = memberWalletInfoService.selectByMemberId(memberId, merchantId);
BigDecimal balance = BigDecimal.ZERO;
if (walletInfo != null) {
BigDecimal principalBalance = walletInfo.getPrincipalBalance() != null ? walletInfo.getPrincipalBalance() : BigDecimal.ZERO;
BigDecimal giftBalance = walletInfo.getGiftBalance() != null ? walletInfo.getGiftBalance() : BigDecimal.ZERO;
balance = principalBalance.add(giftBalance);
}
// 6. 检查余额是否充足
BigDecimal payAmountDecimal = new BigDecimal(payAmount);
if (balance.compareTo(payAmountDecimal) < 0) {
throw new RuntimeException("余额不足");
}
// 7. 生成订单
String tradeNo = IdUtils.fastSimpleUUID();
OrderBasicInfo order = new OrderBasicInfo();
order.setOrderCode(tradeNo);
order.setTransactionCode(tradeNo);
order.setPileSn(pileCode);
order.setConnectorCode(pileCode + "-" + gunNo);
order.setMemberId(memberId);
order.setMerchantId(String.valueOf(pileInfo.getMerchantId()));
order.setStationId(String.valueOf(pileInfo.getStationId()));
order.setOrderStatus("0"); // 待充电(等待启动结果)
order.setPayStatus("0"); // 待支付
order.setChargeStartTime(new Date());
order.setCreateTime(new Date());
order.setPayAmount(payAmountDecimal);
orderBasicInfoService.insert(order);
// 8. 保存交易流水号映射(用于后续查询)
String tradeKey = "jcpp:trade:" + tradeNo;
stringRedisTemplate.opsForValue().set(tradeKey, pileCode, 24, TimeUnit.HOURS);
// 9. 发送远程启动指令(使用新的 Redis 队列方式)
jcppDownlinkService.sendRemoteStartCharging(pileCode, gunNo, tradeNo, payAmount, null, null);
log.info("远程启动充电指令已发送, tradeNo: {}, pileCode: {}, gunNo: {}", tradeNo, pileCode, gunNo);
return tradeNo;
}
@Override
public boolean remoteStopCharging(String memberId, String orderCode) {
log.info("远程停止充电, memberId: {}, orderCode: {}", memberId, orderCode);
// 1. 查询订单
OrderBasicInfo order = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
if (order == null) {
throw new RuntimeException("订单不存在");
}
// 2. 验证会员
if (!memberId.equals(order.getMemberId())) {
throw new RuntimeException("无权操作此订单");
}
// 3. 检查订单状态
if (!"1".equals(order.getOrderStatus())) {
throw new RuntimeException("订单状态不允许停止充电");
}
String pileCode = order.getPileSn();
String gunNo = order.getConnectorCode().replace(pileCode + "-", "");
// 4. 检查充电桩是否在线
if (!jcppDownlinkService.isPileOnline(pileCode)) {
throw new RuntimeException("充电桩离线,请稍后重试");
}
// 5. 发送远程停止指令(使用新的 Redis 队列方式)
jcppDownlinkService.sendRemoteStopCharging(pileCode, gunNo);
log.info("远程停止充电指令已发送, orderCode: {}, pileCode: {}, gunNo: {}", orderCode, pileCode, gunNo);
return true;
}
@Override
public boolean isPileOnline(String pileCode) {
// 使用新的 Redis Set 方式检查在线状态
return jcppDownlinkService.isPileOnline(pileCode);
}
@Override
public String getSessionId(String pileCode) {
String sessionKey = JcppConstants.REDIS_KEY_SESSION + pileCode;
String sessionJson = stringRedisTemplate.opsForValue().get(sessionKey);
if (sessionJson != null) {
JcppSessionInfo sessionInfo = JSON.parseObject(sessionJson, JcppSessionInfo.class);
return sessionInfo.getSessionId();
}
return null;
}
}

View File

@@ -0,0 +1,97 @@
package com.jsowell.pile.jcpp.util;
import com.google.common.hash.Hashing;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* JCPP 消息分区计算器
* 使用 MurmurHash3_128 算法计算消息分区,与 JCPP 保持一致
*
* @author jsowell
*/
@Slf4j
public class JcppPartitionCalculator {
/**
* 默认分区数量
*/
private static final int DEFAULT_PARTITION_COUNT = 10;
/**
* 分区数量(可配置)
*/
private static int partitionCount = DEFAULT_PARTITION_COUNT;
/**
* 设置分区数量
*
* @param count 分区数量
*/
public static void setPartitionCount(int count) {
if (count <= 0) {
throw new IllegalArgumentException("分区数量必须大于 0");
}
partitionCount = count;
log.info("JCPP 消息分区数量设置为: {}", partitionCount);
}
/**
* 获取当前分区数量
*
* @return 分区数量
*/
public static int getPartitionCount() {
return partitionCount;
}
/**
* 根据消息键计算分区编号
* 使用 MurmurHash3_128 算法(与 JCPP 一致)
*
* @param messageKey 消息键(通常是 pileCode
* @return 分区编号0 到 partitionCount-1
*/
public static int getPartition(String messageKey) {
if (messageKey == null || messageKey.isEmpty()) {
log.warn("消息键为空,使用默认分区 0");
return 0;
}
// 使用 MurmurHash3_128 算法计算 hash 值
long hash = Hashing.murmur3_128()
.hashString(messageKey, StandardCharsets.UTF_8)
.asLong();
// 取绝对值并对分区数取模
int partition = Math.abs((int) (hash % partitionCount));
log.debug("消息键: {}, hash: {}, 分区: {}", messageKey, hash, partition);
return partition;
}
/**
* 获取指定分区的队列名称
*
* @param partition 分区编号
* @return 队列名称
*/
public static String getQueueName(int partition) {
return "jcpp.uplink.partition." + partition;
}
/**
* 获取所有分区的队列名称数组
*
* @return 队列名称数组
*/
public static String[] getAllQueueNames() {
String[] queueNames = new String[partitionCount];
for (int i = 0; i < partitionCount; i++) {
queueNames[i] = getQueueName(i);
}
return queueNames;
}
}

View File

@@ -0,0 +1,274 @@
package com.jsowell.pile.jcpp.util;
import com.jsowell.pile.domain.PileBillingDetail;
import com.jsowell.pile.domain.PileBillingTemplate;
import com.jsowell.pile.jcpp.dto.JcppPricingModel;
import com.jsowell.pile.vo.web.BillingDetailVO;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 计费模板转换工具类
*
* @author jsowell
*/
@Slf4j
public class PricingModelConverter {
private PricingModelConverter() {
}
/**
* 将系统计费模板转换为 JCPP 计费模板格式
*
* @param template 系统计费模板
* @return JCPP 计费模板
*/
public static JcppPricingModel convert(PileBillingTemplate template) {
if (template == null) {
return null;
}
JcppPricingModel model = new JcppPricingModel();
model.setPricingId(template.getId());
model.setPricingName(template.getName());
List<PileBillingDetail> details = template.getPileBillingDetailList();
if (details == null || details.isEmpty()) {
// 无详情,使用标准计费
model.setPricingType(1);
return model;
}
// 根据详情判断计费类型
// 如果只有一条记录且时段为全天,则为标准计费
// 否则为峰谷计费或时段计费
if (details.size() == 1 && isAllDay(details.get(0).getApplyTime())) {
model.setPricingType(1);
model.setElectricityPrice(details.get(0).getElectricityPrice());
model.setServicePrice(details.get(0).getServicePrice());
} else {
// 峰谷计费
model.setPricingType(2);
model.setPeakValleyPrice(convertToPeakValley(details));
model.setTimePeriodPrices(convertToTimePeriods(details));
}
return model;
}
/**
* 将 BillingTemplateVO 转换为 JCPP 计费模板格式
*
* @param vo 计费模板 VO
* @return JCPP 计费模板
*/
public static JcppPricingModel convert(BillingTemplateVO vo) {
if (vo == null) {
return null;
}
JcppPricingModel model = new JcppPricingModel();
model.setPricingId(Long.parseLong(vo.getTemplateId()));
model.setPricingName(vo.getTemplateName());
List<BillingDetailVO> details = vo.getBillingDetailList();
if (details == null || details.isEmpty()) {
model.setPricingType(1);
return model;
}
if (details.size() == 1
//&& isAllDay(details.get(0).getApplyTime())
) {
model.setPricingType(1);
model.setElectricityPrice(details.get(0).getElectricityPrice());
model.setServicePrice(details.get(0).getServicePrice());
} else {
model.setPricingType(2);
model.setPeakValleyPrice(convertToPeakValleyFromVO(details));
model.setTimePeriodPrices(convertToTimePeriodsFromVO(details));
}
return model;
}
/**
* 判断是否为全天时段
*/
private static boolean isAllDay(String applyTime) {
if (applyTime == null) {
return true;
}
return "00:00-24:00".equals(applyTime) || "00:00-23:59".equals(applyTime)
|| applyTime.isEmpty();
}
/**
* 转换为峰谷计费明细
*/
private static JcppPricingModel.PeakValleyPrice convertToPeakValley(List<PileBillingDetail> details) {
JcppPricingModel.PeakValleyPrice peakValley = new JcppPricingModel.PeakValleyPrice();
List<JcppPricingModel.TimePeriodConfig> configs = new ArrayList<>();
for (PileBillingDetail detail : details) {
String timeType = detail.getTimeType();
BigDecimal electricityPrice = detail.getElectricityPrice();
BigDecimal servicePrice = detail.getServicePrice();
// 根据时段类型设置价格
switch (timeType) {
case "1": // 尖时
peakValley.setSharpElectricityPrice(electricityPrice);
peakValley.setSharpServicePrice(servicePrice);
break;
case "2": // 峰时
peakValley.setPeakElectricityPrice(electricityPrice);
peakValley.setPeakServicePrice(servicePrice);
break;
case "3": // 平时
peakValley.setFlatElectricityPrice(electricityPrice);
peakValley.setFlatServicePrice(servicePrice);
break;
case "4": // 谷时
peakValley.setValleyElectricityPrice(electricityPrice);
peakValley.setValleyServicePrice(servicePrice);
break;
default:
log.warn("未知的时段类型: {}", timeType);
}
// 解析时段配置
String applyTime = detail.getApplyTime();
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
configs.add(JcppPricingModel.TimePeriodConfig.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.periodType(Integer.parseInt(timeType))
.build());
}
}
}
}
peakValley.setTimePeriodConfigs(configs);
return peakValley;
}
/**
* 转换为时段计费明细
*/
private static List<JcppPricingModel.TimePeriodPrice> convertToTimePeriods(List<PileBillingDetail> details) {
List<JcppPricingModel.TimePeriodPrice> prices = new ArrayList<>();
for (PileBillingDetail detail : details) {
String applyTime = detail.getApplyTime();
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
prices.add(JcppPricingModel.TimePeriodPrice.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.electricityPrice(detail.getElectricityPrice())
.servicePrice(detail.getServicePrice())
.periodType(Integer.parseInt(detail.getTimeType()))
.build());
}
}
}
}
return prices;
}
/**
* 从 VO 转换为峰谷计费明细
*/
private static JcppPricingModel.PeakValleyPrice convertToPeakValleyFromVO(List<BillingDetailVO> details) {
JcppPricingModel.PeakValleyPrice peakValley = new JcppPricingModel.PeakValleyPrice();
List<JcppPricingModel.TimePeriodConfig> configs = new ArrayList<>();
for (BillingDetailVO detail : details) {
String timeType = detail.getTimeType();
BigDecimal electricityPrice = detail.getElectricityPrice();
BigDecimal servicePrice = detail.getServicePrice();
switch (timeType) {
case "1":
peakValley.setSharpElectricityPrice(electricityPrice);
peakValley.setSharpServicePrice(servicePrice);
break;
case "2":
peakValley.setPeakElectricityPrice(electricityPrice);
peakValley.setPeakServicePrice(servicePrice);
break;
case "3":
peakValley.setFlatElectricityPrice(electricityPrice);
peakValley.setFlatServicePrice(servicePrice);
break;
case "4":
peakValley.setValleyElectricityPrice(electricityPrice);
peakValley.setValleyServicePrice(servicePrice);
break;
default:
log.warn("未知的时段类型: {}", timeType);
}
String applyTime = detail.getApplyTime().get(0);
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
configs.add(JcppPricingModel.TimePeriodConfig.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.periodType(Integer.parseInt(timeType))
.build());
}
}
}
}
peakValley.setTimePeriodConfigs(configs);
return peakValley;
}
/**
* 从 VO 转换为时段计费明细
*/
private static List<JcppPricingModel.TimePeriodPrice> convertToTimePeriodsFromVO(List<BillingDetailVO> details) {
List<JcppPricingModel.TimePeriodPrice> prices = new ArrayList<>();
for (BillingDetailVO detail : details) {
String applyTime = detail.getApplyTime().get(0);
if (applyTime != null && !applyTime.isEmpty()) {
String[] periods = applyTime.split(",");
for (String period : periods) {
String[] times = period.split("-");
if (times.length == 2) {
prices.add(JcppPricingModel.TimePeriodPrice.builder()
.startTime(times[0].trim())
.endTime(times[1].trim())
.electricityPrice(detail.getElectricityPrice())
.servicePrice(detail.getServicePrice())
.periodType(Integer.parseInt(detail.getTimeType()))
.build());
}
}
}
}
return prices;
}
}

View File

@@ -0,0 +1,71 @@
package com.jsowell.pile.mapper;
import com.jsowell.pile.domain.JcppSyncRecord;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* JCPP 充电桩同步记录 Mapper 接口
*
* @author jsowell
*/
@Repository
public interface JcppSyncRecordMapper {
/**
* 查询 JCPP 充电桩同步记录
*
* @param id JCPP 充电桩同步记录主键
* @return JCPP 充电桩同步记录
*/
JcppSyncRecord selectJcppSyncRecordById(Long id);
/**
* 查询 JCPP 充电桩同步记录列表
*
* @param jcppSyncRecord JCPP 充电桩同步记录
* @return JCPP 充电桩同步记录集合
*/
List<JcppSyncRecord> selectJcppSyncRecordList(JcppSyncRecord jcppSyncRecord);
/**
* 新增 JCPP 充电桩同步记录
*
* @param jcppSyncRecord JCPP 充电桩同步记录
* @return 结果
*/
int insertJcppSyncRecord(JcppSyncRecord jcppSyncRecord);
/**
* 修改 JCPP 充电桩同步记录
*
* @param jcppSyncRecord JCPP 充电桩同步记录
* @return 结果
*/
int updateJcppSyncRecord(JcppSyncRecord jcppSyncRecord);
/**
* 删除 JCPP 充电桩同步记录
*
* @param id JCPP 充电桩同步记录主键
* @return 结果
*/
int deleteJcppSyncRecordById(Long id);
/**
* 批量删除 JCPP 充电桩同步记录
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteJcppSyncRecordByIds(Long[] ids);
/**
* 查询最后一次成功的同步记录
*
* @param syncType 同步类型
* @return 同步记录
*/
JcppSyncRecord selectLastSuccessRecord(String syncType);
}

View File

@@ -0,0 +1,8 @@
package com.jsowell.pile.service;
import com.jsowell.pile.dto.MerchantOrderReportDTO;
import com.jsowell.pile.vo.web.MerchantOrderReportVO;
public interface BusinessFinancialService {
MerchantOrderReportVO getMyWallet(MerchantOrderReportDTO dto);
}

View File

@@ -0,0 +1,69 @@
package com.jsowell.pile.service.impl;
import com.huifu.adapay.core.exception.BaseAdaPayException;
import com.jsowell.adapay.service.AdapayService;
import com.jsowell.adapay.vo.AdapayAccountBalanceVO;
import com.jsowell.pile.dto.MerchantOrderReportDTO;
import com.jsowell.pile.service.BusinessFinancialService;
import com.jsowell.pile.service.ClearingWithdrawInfoService;
import com.jsowell.pile.service.SettleOrderReportService;
import com.jsowell.pile.vo.web.MerchantOrderReportVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 运营端小程序财务相关Service
*/
@Slf4j
@Service
public class BusinessFinancialServiceImpl implements BusinessFinancialService {
@Autowired
private SettleOrderReportService settleOrderReportService;
@Autowired
private AdapayService adapayService;
@Autowired
private ClearingWithdrawInfoService clearingWithdrawInfoService;
/**
* 我的钱包查询
* @param dto
* @return
*/
@Override
public MerchantOrderReportVO getMyWallet(MerchantOrderReportDTO dto) {
// 查询运营商订单报表
MerchantOrderReportVO result = settleOrderReportService.getMerchantOrderReportV2(dto);
// 查询账户余额
BigDecimal acctBalance = BigDecimal.ZERO;
try {
AdapayAccountBalanceVO accountBalanceVO = adapayService.queryAdapayAccountBalance(dto.getMerchantId());
if (accountBalanceVO != null && accountBalanceVO.getAcctBalance() != null) {
acctBalance = accountBalanceVO.getAcctBalance();
}
} catch (BaseAdaPayException e) {
log.error("查询汇付账户余额异常 merchantId:{}", dto.getMerchantId(), e);
}
result.getMerchantOrderReport().setAcctBalance(acctBalance);
// 查询累计提现金额
BigDecimal totalWithdraw = BigDecimal.ZERO;
try {
BigDecimal withdraw = clearingWithdrawInfoService.queryTotalWithdraw(dto.getMerchantId());
if (withdraw != null) {
totalWithdraw = withdraw;
}
} catch (Exception e) {
log.error("查询累计提现金额异常 merchantId:{}", dto.getMerchantId(), e);
}
result.getMerchantOrderReport().setTotalWithdraw(totalWithdraw);
return result;
}
}

View File

@@ -816,6 +816,10 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
}
}
OrderBasicInfo orderInfoByOrderCode = orderBasicInfoMapper.getOrderInfoByOrderCode(orderBasicInfo.getOrderCode());
orderBasicInfo.setPayMode(orderInfoByOrderCode.getPayMode());
// 判断订单的支付方式
String payMode = orderBasicInfo.getPayMode();
if (StringUtils.equals(payMode, OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue())) {

View File

@@ -30,14 +30,32 @@ public class PileModelInfoServiceImpl implements PileModelInfoService {
private RedisCache redisCache;
/**
* 查询充电桩型号信息
* 查询充电桩型号信息(带缓存)
*
* @param id 充电桩型号信息主键
* @return 充电桩型号信息
*/
@Override
public PileModelInfo selectPileModelInfoById(Long id) {
return pileModelInfoMapper.selectPileModelInfoById(id);
if (id == null) {
return null;
}
// 1. 尝试从缓存获取
String redisKey = CacheConstants.PILE_MODEL_INFO_BY_ID + id;
PileModelInfo modelInfo = redisCache.getCacheObject(redisKey);
// 2. 缓存未命中,从数据库查询
if (modelInfo == null) {
modelInfo = pileModelInfoMapper.selectPileModelInfoById(id);
// 3. 查询结果存入缓存1天有效期
if (modelInfo != null) {
redisCache.setCacheObject(redisKey, modelInfo, CacheConstants.cache_expire_time_1d);
}
}
return modelInfo;
}
/**
@@ -135,10 +153,18 @@ public class PileModelInfoServiceImpl implements PileModelInfoService {
return modelInfoVO;
}
/**
* 清除缓存
*
* @param modelIds 型号ID数组
*/
private void cleanCache(Long[] modelIds) {
List<String> redisKeyList = Lists.newArrayList();
for (Long modelId : modelIds) {
// 清除 getPileModelInfoByModelId 的缓存
redisKeyList.add(CacheConstants.GET_PILE_MODEL_INFO_BY_MODEL_ID + modelId);
// 清除 selectPileModelInfoById 的缓存
redisKeyList.add(CacheConstants.PILE_MODEL_INFO_BY_ID + modelId);
}
redisCache.deleteObject(redisKeyList);
}

View File

@@ -42,4 +42,14 @@ public class OrderReportDetail {
// 他人分账金额
private BigDecimal otherSplitAmount;
/**
* 账户总余额
*/
private BigDecimal acctBalance;
/**
* 累计提现金额
*/
private BigDecimal totalWithdraw;
}