2024-10-08 09:38:54 +08:00
|
|
|
|
/**
|
2025-03-04 10:42:17 +08:00
|
|
|
|
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
|
|
|
|
|
|
* 微信:mohan_88888
|
|
|
|
|
|
* 抖音:程序员三丙
|
|
|
|
|
|
* 付费课程知识星球:https://t.zsxq.com/aKtXo
|
2024-10-08 09:38:54 +08:00
|
|
|
|
*/
|
|
|
|
|
|
package sanbing.jcpp.protocol.yunkuaichong;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.text.CharSequenceUtil;
|
|
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
|
import sanbing.jcpp.infrastructure.util.codec.BCDUtil;
|
|
|
|
|
|
import sanbing.jcpp.proto.gen.ProtocolProto;
|
|
|
|
|
|
import sanbing.jcpp.protocol.listener.tcp.TcpSession;
|
|
|
|
|
|
import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength;
|
2024-10-12 16:47:27 +08:00
|
|
|
|
import sanbing.jcpp.protocol.yunkuaichong.enums.YunKuaiChongDownlinkCmdEnum;
|
2024-10-08 09:38:54 +08:00
|
|
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
|
import java.math.RoundingMode;
|
2025-08-13 20:02:41 +08:00
|
|
|
|
import java.nio.charset.StandardCharsets;
|
2024-10-08 09:38:54 +08:00
|
|
|
|
import java.text.DecimalFormat;
|
|
|
|
|
|
import java.time.LocalTime;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
import static sanbing.jcpp.infrastructure.util.codec.ByteUtil.crcSum;
|
|
|
|
|
|
import static sanbing.jcpp.infrastructure.util.codec.ByteUtil.toBytes;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @author baigod
|
|
|
|
|
|
*/
|
|
|
|
|
|
public class AbstractYunKuaiChongCmdExe {
|
|
|
|
|
|
|
|
|
|
|
|
private static final byte TOP_BYTE = 0x00;
|
|
|
|
|
|
private static final byte PEAK_BYTE = 0x01;
|
|
|
|
|
|
private static final byte FLAT_BYTE = 0x02;
|
|
|
|
|
|
private static final byte VALLEY_BYTE = 0x03;
|
|
|
|
|
|
|
|
|
|
|
|
protected static final byte YUNKUAICHONG_HEAD = 0x68;
|
|
|
|
|
|
protected static final int YUNKUAICHONG_NORMAL_ENCRYPTION_FLAG = 0;
|
|
|
|
|
|
|
|
|
|
|
|
private static final DecimalFormat PRICING_ID_DECIMAL_FORMAT = new DecimalFormat("0000");
|
|
|
|
|
|
|
|
|
|
|
|
protected static String decodeTradeNo(byte[] tradeNo) {
|
|
|
|
|
|
String tradeNoStr = BCDUtil.toString(tradeNo);
|
|
|
|
|
|
return CharSequenceUtil.strip(tradeNoStr, "0", null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected byte[] encodePricingId(long pricingId) {
|
|
|
|
|
|
return BCDUtil.toBytes(PRICING_ID_DECIMAL_FORMAT.format(pricingId % 10000));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static byte getFlagForCurrentTime(List<ProtocolProto.PeriodProto> periodList, LocalTime currentTime) {
|
|
|
|
|
|
for (ProtocolProto.PeriodProto period : periodList) {
|
|
|
|
|
|
LocalTime beginLt = LocalTime.parse(period.getBegin());
|
|
|
|
|
|
LocalTime endLt = "00:00".equals(period.getEnd()) ? LocalTime.MAX : LocalTime.parse(period.getEnd());
|
|
|
|
|
|
if ((currentTime.equals(beginLt) || currentTime.isAfter(beginLt)) && currentTime.isBefore(endLt)) {
|
|
|
|
|
|
switch (period.getFlag()) {
|
|
|
|
|
|
case TOP:
|
|
|
|
|
|
return TOP_BYTE;
|
|
|
|
|
|
case PEAK:
|
|
|
|
|
|
return PEAK_BYTE;
|
|
|
|
|
|
case VALLEY:
|
|
|
|
|
|
return VALLEY_BYTE;
|
2024-10-11 15:27:42 +08:00
|
|
|
|
default:
|
|
|
|
|
|
return FLAT_BYTE;
|
2024-10-08 09:38:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return FLAT_BYTE; // 默认情况下返回平价
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static byte[] encodePileCode(String pileCode) {
|
|
|
|
|
|
if (StringUtils.length(pileCode) > 32) {
|
2025-08-09 11:00:12 +00:00
|
|
|
|
throw new IllegalArgumentException("云快充可接受最大桩编号为14位");
|
2024-10-08 09:38:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String pileCodeStr = StringUtils.leftPad(pileCode, 14, '0');
|
|
|
|
|
|
|
|
|
|
|
|
return BCDUtil.toBytes(pileCodeStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static byte[] encodeGunCode(String gunCode) {
|
|
|
|
|
|
if (StringUtils.length(gunCode) > 2) {
|
2025-08-09 11:00:12 +00:00
|
|
|
|
throw new IllegalArgumentException("云快充可接受最大枪编号为2位");
|
2024-10-08 09:38:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String gunCodeStr = StringUtils.leftPad(gunCode, 2, '0');
|
|
|
|
|
|
|
|
|
|
|
|
return BCDUtil.toBytes(gunCodeStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static byte[] encodeTradeNo(String tradeNo) {
|
|
|
|
|
|
if (StringUtils.length(tradeNo) > 32) {
|
|
|
|
|
|
throw new IllegalArgumentException("云快充1.5可接受最大交易流水号为32位");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String tradeNoStr = StringUtils.leftPad(tradeNo, 32, '0');
|
|
|
|
|
|
|
|
|
|
|
|
return BCDUtil.toBytes(tradeNoStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-10-12 16:47:27 +08:00
|
|
|
|
protected byte[] encode(YunKuaiChongDownlinkCmdEnum downlinkCmd,
|
2024-10-08 09:38:54 +08:00
|
|
|
|
int seqNo,
|
|
|
|
|
|
int encryptionFlag,
|
|
|
|
|
|
ByteBuf msgBody) {
|
|
|
|
|
|
int msgBodyLength = msgBody.readableBytes();
|
|
|
|
|
|
ByteBuf response = Unpooled.buffer(msgBodyLength + 6);
|
|
|
|
|
|
response.writeByte(YUNKUAICHONG_HEAD);
|
|
|
|
|
|
response.writeByte(msgBodyLength + 4);
|
|
|
|
|
|
response.writeShortLE(seqNo);
|
|
|
|
|
|
response.writeByte(encryptionFlag);
|
|
|
|
|
|
response.writeByte(downlinkCmd.getCmd());
|
|
|
|
|
|
response.writeBytes(msgBody);
|
|
|
|
|
|
|
|
|
|
|
|
// 帧校验域:从序列号域到数据域的 CRC 校验,校验多项式为 0x180D,低字节在前,高字节在后
|
|
|
|
|
|
byte[] checkArr = new byte[msgBodyLength + 4];
|
|
|
|
|
|
System.arraycopy(response.array(), 2, checkArr, 0, checkArr.length);
|
|
|
|
|
|
|
|
|
|
|
|
response.writeShortLE(crcSum(checkArr));
|
|
|
|
|
|
|
|
|
|
|
|
return toBytes(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-12 16:47:27 +08:00
|
|
|
|
protected void encodeAndWriteFlush(YunKuaiChongDownlinkCmdEnum downlinkCmd,
|
2024-10-09 10:08:32 +08:00
|
|
|
|
int seqNo,
|
|
|
|
|
|
int encryptionFlag,
|
|
|
|
|
|
ByteBuf msgBody,
|
|
|
|
|
|
TcpSession tcpSession) {
|
2024-10-08 09:38:54 +08:00
|
|
|
|
|
|
|
|
|
|
byte[] encode = encode(downlinkCmd, seqNo, encryptionFlag, msgBody);
|
|
|
|
|
|
|
|
|
|
|
|
tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-12 16:47:27 +08:00
|
|
|
|
protected void encodeAndWriteFlush(YunKuaiChongDownlinkCmdEnum downlinkCmd,
|
2024-10-09 10:08:32 +08:00
|
|
|
|
ByteBuf msgBody,
|
|
|
|
|
|
TcpSession tcpSession) {
|
2024-10-08 09:38:54 +08:00
|
|
|
|
|
|
|
|
|
|
byte[] encode = encode(downlinkCmd,
|
|
|
|
|
|
tcpSession.nextSeqNo(SequenceNumberLength.SHORT),
|
|
|
|
|
|
YUNKUAICHONG_NORMAL_ENCRYPTION_FLAG,
|
|
|
|
|
|
msgBody);
|
|
|
|
|
|
|
|
|
|
|
|
tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static BigDecimal reduceMagnification(long value, int magnification) {
|
|
|
|
|
|
return new BigDecimal(value).divide(new BigDecimal(magnification), 4, RoundingMode.HALF_UP);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected static BigDecimal reduceMagnification(long value, int magnification, int scale) {
|
|
|
|
|
|
return new BigDecimal(value).divide(new BigDecimal(magnification), scale, RoundingMode.HALF_UP);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 20:02:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 高性能版本:直接写入目标ByteBuf,避免创建中间ByteBuf对象
|
|
|
|
|
|
* 将字符串参数写入ByteBuf,不足指定长度用0填充(右侧填充)
|
|
|
|
|
|
* 使用ASCII编码,适用于协议中的字符串字段
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param target 目标ByteBuf
|
|
|
|
|
|
* @param param 要写入的字符串参数,可以为null
|
|
|
|
|
|
* @param maxLength 目标长度(字节数)
|
|
|
|
|
|
* @throws IllegalArgumentException 如果参数字节长度超过maxLength
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static void writeParamFillZero(ByteBuf target, String param, int maxLength) {
|
|
|
|
|
|
if (maxLength <= 0) {
|
|
|
|
|
|
throw new IllegalArgumentException("maxLength must be positive");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (param == null) {
|
|
|
|
|
|
param = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用ASCII编码,适用于协议字段
|
|
|
|
|
|
byte[] paramBytes = param.getBytes(StandardCharsets.US_ASCII);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查长度
|
|
|
|
|
|
if (paramBytes.length > maxLength) {
|
|
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
|
|
String.format("云快充1.5.0 参数: %s 字节长度 %d 超过最大长度 %d",
|
|
|
|
|
|
param, paramBytes.length, maxLength));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保目标ByteBuf有足够的写入空间
|
|
|
|
|
|
target.ensureWritable(maxLength);
|
|
|
|
|
|
|
|
|
|
|
|
// 写入数据
|
|
|
|
|
|
target.writeBytes(paramBytes);
|
|
|
|
|
|
|
|
|
|
|
|
// 填充剩余空间
|
|
|
|
|
|
int remainingBytes = maxLength - paramBytes.length;
|
|
|
|
|
|
if (remainingBytes > 0) {
|
|
|
|
|
|
target.writeZero(remainingBytes);
|
2025-08-13 10:47:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 20:02:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 高性能版本:将整数参数作为字节值直接写入ByteBuf,不足指定长度用0填充(右侧填充)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param target 目标ByteBuf
|
|
|
|
|
|
* @param param 要写入的整数参数(将作为字节值写入,只使用低8位)
|
|
|
|
|
|
* @param maxLength 目标长度(字节数)
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected static void writeParamFillZero(ByteBuf target, int param, int maxLength) {
|
|
|
|
|
|
if (maxLength <= 0) {
|
|
|
|
|
|
throw new IllegalArgumentException("maxLength must be positive");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保目标ByteBuf有足够的写入空间
|
|
|
|
|
|
target.ensureWritable(maxLength);
|
|
|
|
|
|
|
|
|
|
|
|
// 将整数作为字节值写入(只使用低8位)
|
|
|
|
|
|
target.writeByte(param & 0xFF);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算剩余空间并填充0
|
|
|
|
|
|
int remainingBytes = maxLength - 1; // 已经写入了1个字节
|
|
|
|
|
|
if (remainingBytes > 0) {
|
|
|
|
|
|
target.writeZero(remainingBytes);
|
|
|
|
|
|
}
|
2025-08-13 10:47:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-08 09:38:54 +08:00
|
|
|
|
}
|