This commit is contained in:
2023-03-04 16:29:55 +08:00
commit 397ba75479
1007 changed files with 109050 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
package com.jsowell.netty.service.yunkuaichong;
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
/**
* 云快充处理service
*/
public interface YKCBusinessService {
/**
* 处理桩发来的请求
* 不需要应答的返回null
* @param msg 请求报文
* @param channel 通道信息
* @return 结果
*/
byte[] process(byte[] msg, Channel channel);
/**
* 桩退出
* @param channelId channelId
*/
void exit(ChannelId channelId);
}

View File

@@ -0,0 +1,64 @@
package com.jsowell.netty.service.yunkuaichong;
import com.jsowell.netty.command.ykc.GetRealTimeMonitorDataCommand;
import com.jsowell.netty.command.ykc.IssueQRCodeCommand;
import com.jsowell.netty.command.ykc.ProofreadTimeCommand;
import com.jsowell.netty.command.ykc.PublishPileBillingTemplateCommand;
import com.jsowell.netty.command.ykc.RebootCommand;
import com.jsowell.netty.command.ykc.StartChargingCommand;
import com.jsowell.netty.command.ykc.StopChargingCommand;
import com.jsowell.netty.command.ykc.UpdateFileCommand;
/**
* 云快充协议向充电桩发送命令service
*/
public interface YKCPushCommandService {
/**
* 发送启动充电指令
* @param startChargingCommand
*/
void pushStartChargingCommand(StartChargingCommand startChargingCommand);
/**
* 发送停止充电指令
* @param stopChargingCommand
*/
void pushStopChargingCommand(StopChargingCommand stopChargingCommand);
/**
* 读取实时监测数据命令
* @param command
*/
void pushGetRealTimeMonitorDataCommand(GetRealTimeMonitorDataCommand command);
/**
* 发送重启指令
* @param command
*/
void pushRebootCommand(RebootCommand command);
/**
* 发送下发二维码命令
* @param command
*/
void pushIssueQRCodeCommand(IssueQRCodeCommand command);
/**
* 发送对时命令
* @param command
*/
void pushProofreadTimeCommand(ProofreadTimeCommand command);
/**
* 下发计费模板命令
* @param command
*/
void pushPublishPileBillingTemplate(PublishPileBillingTemplateCommand command);
/**
* 发送远程更新命令
* @param command
*/
void pushUpdateFileCommand(UpdateFileCommand command);
}

View File

@@ -0,0 +1,100 @@
package com.jsowell.netty.service.yunkuaichong.impl;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.enums.ykc.OrderStatusEnum;
import com.jsowell.common.enums.ykc.PileChannelEntity;
import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.netty.factory.YKCOperateFactory;
import com.jsowell.netty.handler.AbstractHandler;
import com.jsowell.netty.service.yunkuaichong.YKCBusinessService;
import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.service.IOrderBasicInfoService;
import com.jsowell.pile.service.IPileConnectorInfoService;
import com.jsowell.pile.service.IPileMsgRecordService;
import com.jsowell.pile.vo.web.OrderListVO;
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class YKCBusinessServiceImpl implements YKCBusinessService {
@Autowired
private IPileMsgRecordService pileMsgRecordService;
@Autowired
private IPileConnectorInfoService pileConnectorInfoService;
@Autowired
private IOrderBasicInfoService orderBasicInfoService;
@Autowired
private YKCPushCommandService ykcPushCommandService;
@Override
public byte[] process(byte[] msg, Channel channel) {
if (!YKCUtils.checkMsg(msg)) {
// 校验不通过,丢弃消息
return null;
}
YKCDataProtocol ykcDataProtocol = new YKCDataProtocol(msg);
// 获取帧类型
String frameType = YKCUtils.frameType2Str(ykcDataProtocol.getFrameType());
// 获取业务处理handler
AbstractHandler invokeStrategy = YKCOperateFactory.getInvokeStrategy(frameType);
return invokeStrategy.supplyProcess(ykcDataProtocol, channel);
}
@Override
public void exit(ChannelId channelId) {
// 获取桩编号
String pileSn = PileChannelEntity.getPileSnByChannelId(channelId.asLongText());
if (StringUtils.isBlank(pileSn)) {
return;
}
log.info("充电桩退出:{}, channelId:{}", pileSn, PileChannelEntity.getChannelByPileSn(pileSn).id());
// 充电桩断开连接,所有枪口都设置为【离线】
pileConnectorInfoService.updateConnectorStatusByPileSn(pileSn, PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue());
// 将此桩正在进行充电的订单状态改为 异常
List<OrderListVO> orderListVOS = orderBasicInfoService.selectChargingOrder(pileSn);
if (CollectionUtils.isNotEmpty(orderListVOS)) {
for (OrderListVO orderListVO : orderListVOS) {
if (StringUtils.equals(orderListVO.getOrderStatus(), OrderStatusEnum.IN_THE_CHARGING.getValue())) {
// 修改数据库订单状态
OrderBasicInfo info = OrderBasicInfo.builder()
.id(Long.parseLong(orderListVO.getId()))
.orderStatus(OrderStatusEnum.ABNORMAL.getValue())
.build();
orderBasicInfoService.updateOrderBasicInfo(info);
log.info("充电桩:{}退出, 修改充电桩正在充电的订单状态为异常, orderCode: {}", pileSn, orderListVO.getOrderCode());
}
}
}
// 记录充电桩退出msg
// 保存报文
String type = YKCFrameTypeCode.PILE_LOG_OUT.getCode() + "";
String jsonMsg = YKCFrameTypeCode.PILE_LOG_OUT.getValue();
pileMsgRecordService.save(pileSn, pileSn, type, jsonMsg, "");
// 自动重启 发送重启指令
// RebootCommand command = RebootCommand.builder().pileSn(pileSn).build();
// ykcPushCommandService.pushRebootCommand(command);
// 删除桩编号和channel的关系
PileChannelEntity.removeByPileSn(pileSn);
}
}

View File

@@ -0,0 +1,341 @@
package com.jsowell.netty.service.yunkuaichong.impl;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.core.redis.RedisCache;
import com.jsowell.common.enums.ykc.PileChannelEntity;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.CRC16Util;
import com.jsowell.common.util.Cp56Time2a.Cp56Time2aUtil;
import com.jsowell.common.util.DateUtils;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.netty.command.ykc.GetRealTimeMonitorDataCommand;
import com.jsowell.netty.command.ykc.IssueQRCodeCommand;
import com.jsowell.netty.command.ykc.ProofreadTimeCommand;
import com.jsowell.netty.command.ykc.PublishPileBillingTemplateCommand;
import com.jsowell.netty.command.ykc.RebootCommand;
import com.jsowell.netty.command.ykc.StartChargingCommand;
import com.jsowell.netty.command.ykc.StopChargingCommand;
import com.jsowell.netty.command.ykc.UpdateFileCommand;
import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService;
import com.jsowell.pile.service.IPileBasicInfoService;
import com.jsowell.pile.service.IPileBillingTemplateService;
import com.jsowell.pile.service.IPileConnectorInfoService;
import com.jsowell.pile.service.IPileModelInfoService;
import com.jsowell.pile.service.IPileMsgRecordService;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import com.jsowell.pile.vo.web.PileModelInfoVO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
public class YKCPushCommandServiceImpl implements YKCPushCommandService {
@Autowired
private IPileBillingTemplateService pileBillingTemplateService;
@Autowired
private IPileModelInfoService pileModelInfoService;
@Autowired
private IPileBasicInfoService pileBasicInfoService;
@Autowired
private RedisCache redisCache;
@Autowired
private IPileMsgRecordService pileMsgRecordService;
@Autowired
private IPileConnectorInfoService pileConnectorInfoService;
// 需要记录报文的数据帧类型
private final List<String> frameTypeList = Lists.newArrayList(
YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_RESTART_CODE.getBytes()),
YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_CONTROL_START_CODE.getBytes()),
YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_STOP_CHARGING_CODE.getBytes())
);
public boolean push(byte[] msg, String pileSn, Enum<YKCFrameTypeCode> frameTypeCode) {
// 通过桩编号获取channel
Channel channel = PileChannelEntity.getChannelByPileSn(pileSn);
if (Objects.isNull(channel)) {
log.error("push命令失败, 桩号:{}无法获取到长连接, 请检查充电桩连接状态!", pileSn);
return false;
}
/**
* 拼接报文
*/
// 起始标志
byte[] head = new byte[]{0x68};
// 序列号域
byte[] serialNumber = new byte[]{0x00, 0x00};
// 加密标志
byte[] encryptFlag = new byte[]{0x00};
// 帧类型标志
byte[] frameType = new byte[]{(byte) ((YKCFrameTypeCode) frameTypeCode).getCode()};
// 序列号域+加密标志+帧类型标志+消息体
byte[] temp = Bytes.concat(serialNumber, encryptFlag, frameType, msg);
// 数据长度
byte[] length = BytesUtil.intToBytes(temp.length, 1);
// 帧校验域
byte[] crc = BytesUtil.intToBytes(CRC16Util.calcCrc16(temp));
// 返回报文
byte[] writeMsg = Bytes.concat(head, length, temp, crc);
// 返回完整的报文 string类型
String wholeMsg= BytesUtil.binary(writeMsg, 16);
log.info("[" + channel.remoteAddress() + "] 主动发送push请求信息:{}", wholeMsg);
ByteBuf byteBuf = channel.alloc().buffer().writeBytes(writeMsg);
ChannelFuture channelFuture = channel.writeAndFlush(byteBuf);
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// 检查操作的状态
if (channelFuture.isSuccess()) {
log.info("push结果【成功】, remoteAddress:{}, channelId:{}, 报文:{}, ", channel.remoteAddress(), channel.id(), wholeMsg);
} else {
// 如果发生错误则访问描述原因的Throwable
Throwable cause = channelFuture.cause();
cause.printStackTrace();
log.info("push结果【失败】, remoteAddress:{}, channelId:{}, 报文:{}", channel.remoteAddress(), channel.id(), wholeMsg);
log.error("push发送命令失败", cause);
}
}
});
// 保存报文
String frameTypeStr = YKCUtils.frameType2Str(((YKCFrameTypeCode) frameTypeCode).getBytes());
if (frameTypeList.contains(frameTypeStr)) {
pileMsgRecordService.save(pileSn, null, frameTypeStr, null, wholeMsg);
}
return true;
}
/**
* 发送启动充电指令
*/
@Override
public void pushStartChargingCommand(StartChargingCommand command) {
String pileSn = command.getPileSn();
String connectorCode = command.getConnectorCode();
String orderCode = command.getOrderCode();
if (StringUtils.isEmpty(pileSn) || StringUtils.isEmpty(connectorCode) ) {
log.warn("远程启动充电, 充电桩编号和枪口号不能为空");
return;
}
if (StringUtils.isEmpty(orderCode)) {
log.warn("远程启动充电, 交易流水号不能为空");
return;
}
if (command.getChargeAmount() == null || BigDecimal.ZERO.equals(command.getChargeAmount())) {
log.warn("远程启动充电, 充电金额不能为0");
return;
}
// 枪口号
byte[] connectorCodeByteArr = BytesUtil.str2Bcd(connectorCode);
// 交易流水号
byte[] orderIdByteArr = BytesUtil.str2Bcd(orderCode);
// 桩编号
byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn);
// 逻辑卡号
String logicCardNum = StringUtils.isBlank(command.getLogicCardNum())
? Constants.ZERO
: command.getLogicCardNum();
byte[] logicCardNumByteArr = BytesUtil.checkLengthAndFrontAppendZero(BytesUtil.str2Bcd(logicCardNum), 16);
// 物理卡号
String physicsCardNum = StringUtils.isBlank(command.getPhysicsCardNum())
? Constants.ZERO
: command.getPhysicsCardNum();
byte[] physicsCardNumByteArr = BytesUtil.checkLengthAndFrontAppendZero(BytesUtil.str2Bcd(physicsCardNum), 16);
// 账户余额
BigDecimal chargeAmount = command.getChargeAmount();
byte[] accountBalanceByteArr = YKCUtils.getPriceByte(chargeAmount.toString(), 2);
byte[] msgBody = Bytes.concat(orderIdByteArr, pileSnByteArr, connectorCodeByteArr, logicCardNumByteArr, physicsCardNumByteArr, accountBalanceByteArr);
this.push(msgBody, pileSn, YKCFrameTypeCode.REMOTE_CONTROL_START_CODE);
log.info("【=====平台下发充电指令=====】:订单id:{}, 桩号:{}, 枪口号:{}, 逻辑卡号:{}, 物理卡号:{}, 账户余额:{}",
orderCode, pileSn, BytesUtil.bcd2Str(connectorCodeByteArr), logicCardNum, physicsCardNum, chargeAmount);
}
@Override
public void pushStopChargingCommand(StopChargingCommand command) {
String pileSn = command.getPileSn();
String connectorCode = command.getConnectorCode();
// 远程停机
byte[] msgBody = Bytes.concat(BytesUtil.str2Bcd(pileSn), BytesUtil.str2Bcd(connectorCode));
this.push(msgBody, pileSn, YKCFrameTypeCode.REMOTE_STOP_CHARGING_CODE);
log.info("【=====平台下发指令=====】:远程停止充电,桩号:{},枪口号:{}", pileSn, connectorCode);
}
@Override
public void pushGetRealTimeMonitorDataCommand(GetRealTimeMonitorDataCommand command) {
String pileSn = command.getPileSn();
String connectorCode = command.getConnectorCode();
byte[] msg = BytesUtil.str2Bcd(pileSn + connectorCode);
this.push(msg, pileSn, YKCFrameTypeCode.READ_REAL_TIME_MONITOR_DATA_CODE);
log.info("【=====平台下发指令=====】:获取充电桩:{} 的 {} 枪口实时数据信息", pileSn, connectorCode);
}
@Override
public void pushRebootCommand(RebootCommand command) {
String pileSn = command.getPileSn();
byte[] msg = BytesUtil.str2Bcd(pileSn + Constants.ZERO_ONE);
log.info("【=====平台下发指令=====】:重启充电桩:,{}", pileSn);
this.push(msg, pileSn, YKCFrameTypeCode.REMOTE_RESTART_CODE);
}
// 下发二维码
@Override
public void pushIssueQRCodeCommand(IssueQRCodeCommand command) {
log.info("异步下发二维码 thread:{}", Thread.currentThread().getName());
String pileSn = command.getPileSn();
// 桩编码
byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn);
// 二维码格式 0x00第一种 前缀+桩编号 0x01第二种 前缀+桩编号+枪编号
byte[] qrCodeTypeByteArr = Constants.oneByteArray;
// 二维码前缀 如“www.baidu.comNo=”
// String qrCodePrefix = "https://wx.charging.shbochong.cn/prepare_charge?code=";
// String qrCodePrefix = pileBasicInfoService.getPileQrCodeUrl(null);
String qrCodePrefix = pileConnectorInfoService.getPileConnectorQrCodeUrl(null);
byte[] qrCodePrefixByteArr = BytesUtil.str2Asc(qrCodePrefix);
// 二维码前缀长度 二维码前缀长度长度最大不超过200 字节
int length = qrCodePrefix.length();
byte[] qrCodePrefixLengthByteArr = BytesUtil.intToBytes(length, 1);
// 拼接消息体
byte[] msg = Bytes.concat(pileSnByteArr, qrCodeTypeByteArr, qrCodePrefixLengthByteArr, qrCodePrefixByteArr);
// push消息
boolean result = this.push(msg, pileSn, YKCFrameTypeCode.REMOTE_ISSUE_QRCODE_CODE);
log.info("=====平台下发指令===== :下发二维码,地址为:{}", qrCodePrefix);
}
/**
* 0x56 对时设置
* @param command
*/
@Override
public void pushProofreadTimeCommand(ProofreadTimeCommand command) {
log.info("充电桩对时thread:{}", Thread.currentThread().getName());
String pileSn = command.getPileSn();
// Date date = new Date();
// Date parseDate = DateUtils.parseDate("2023-02-28 16:45:20");
Date date = DateUtils.parseDate(DateUtils.getTime());
// 根据不同程序版本获取工具类
// String programVersion = redisCache.getCacheMapValue(CacheConstants.PILE_PROGRAM_VERSION, pileSn);
// AbsCp56Time2aUtil cp56Time2aUtil = Cp56Time2aFactory.getInvokeStrategy(programVersion);
// String encodeCP56Time2a = cp56Time2aUtil.date2HexStr(date);
byte[] bytes = Cp56Time2aUtil.date2Hbyte(date);
byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn);
byte[] msg = Bytes.concat(pileSnByteArr, bytes);
this.push(msg, pileSn, YKCFrameTypeCode.TIME_CHECK_SETTING_CODE);
log.info("[充电桩:{}对时, 时间:{}, CP56Time2a:{}]", pileSn, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date), BytesUtil.binary(bytes, 16));
}
@Override
public void pushPublishPileBillingTemplate(PublishPileBillingTemplateCommand command) {
BillingTemplateVO billingTemplateVO = command.getBillingTemplateVO();
String pileSn = command.getPileSn();
// 转换
byte[] messageBody = pileBillingTemplateService.generateBillingTemplateMsgBody(pileSn, billingTemplateVO);
// 发送
if (messageBody != null) {
this.push(messageBody, pileSn, YKCFrameTypeCode.BILLING_TEMPLATE_SETTING_CODE);
}
}
@Override
public void pushUpdateFileCommand(UpdateFileCommand command) {
List<String> pileSns = command.getPileSnList();
if (CollectionUtils.isEmpty(pileSns)) {
return;
}
List<PileModelInfoVO> list = pileModelInfoService.getPileModelInfoByPileSnList(pileSns);
if (CollectionUtils.isEmpty(list)) {
return;
}
// 获取桩型号 01直流 02交流
byte[] pileModelType;
for (PileModelInfoVO pileModelInfoVO : list) {
byte[] pileSnByteArr = BytesUtil.str2Bcd(pileModelInfoVO.getPileSn());
// 数据库 1- 快充(直流) 2-慢充(交流)
/*if (StringUtils.equals(pileModelInfoVO.getSpeedType(), Constants.ONE)) {
pileModelType = Constants.oneByteArray;
} else {
pileModelType = Constants.twoByteArray;
}*/
pileModelType = Constants.zeroByteArray;
// 额定功率
String ratedPower = pileModelInfoVO.getRatedPower();
int i = Integer.parseInt(ratedPower);
// byte[] ratedPowerByteArr = Base64.getDecoder().decode(ratedPower);
byte[] ratedPowerByteArr = BytesUtil.checkLengthAndBehindAppendZero(Constants.zeroByteArray, 4);
// 升级服务器地址
byte[] updateServerAddressByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerIP), 32);
// 升级服务器端口
byte[] updateServerPortByteArr = BytesUtil.checkLengthAndBehindAppendZero(Constants.updateServerPort, 4);
// byte[] updateServerPortByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Bcd("15"), 4);
// 用户名
byte[] userNameByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerUserName), 32);
// 密码
byte[] passwordByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerPassword), 32);
// 文件路径
byte[] filePathByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.filePath), 64);
// 执行控制 01立即执行 02空闲执行
byte[] performTypeByteArr = Constants.oneByteArray;
// 下载超时时间 单位min
byte[] overTimeByteArr = new byte[]{0x05};
byte[] msgBody = Bytes.concat(pileSnByteArr, pileModelType, ratedPowerByteArr, updateServerAddressByteArr,
updateServerPortByteArr, userNameByteArr, passwordByteArr, filePathByteArr, performTypeByteArr, overTimeByteArr);
this.push(msgBody, pileModelInfoVO.getPileSn(), YKCFrameTypeCode.REMOTE_UPDATE_CODE);
log.info("【=====平台下发指令=====】:远程更新, 桩号:{}, 类型:{}, 额定功率:{}, 服务器地址:{}, 端口号:{}, 用户名:{}, 密码:{}, 文件路径:{}",
pileModelInfoVO.getPileSn(), pileModelType, BytesUtil.bcd2Str(ratedPowerByteArr), BytesUtil.binary(updateServerAddressByteArr, 16),
BytesUtil.binary(updateServerPortByteArr, 16), BytesUtil.binary(userNameByteArr, 16), BytesUtil.binary(passwordByteArr, 16),
BytesUtil.binary(filePathByteArr, 16));
}
}
}