Ver código fonte

feat(admin): 为缓存控制器添加Swagger文档注解

- 在CacheController类上添加@Api注解标记缓存监控模块
- 为getInfo方法添加@ApiOperation注解描述缓存列表功能
- 为cache方法添加@ApiOperation注解描述缓存名称列表功能
- 为getCacheKeys方法添加@ApiOperation注解描述缓存键列表功能
- 为getCacheValue方法添加@ApiOperation注解描述缓存值获取功能
- 为clearCacheName方法添加@ApiOperation注解描述缓存名称清除功能
- 为clearCacheKey方法添加@ApiOperation注解描述缓存键清除功能
- 为clearCacheAll方法添加@ApiOperation注解描述缓存全清功能
- 在CaptchaController类上添加@Api注解标记验证码模块
- 为getCode方法添加@ApiOperation注解描述生成验证码功能
- 在CommonController类上添加@Api注解标记通用接口模块
- 为fileDownload方法添加@ApiOperation注解描述通用下载功能
- 为uploadFile方法添加@ApiOperation注解描述单个文件上传功能
- 为uploadFiles方法添加@ApiOperation注解描述多个文件上传功能
- 为resourceDownload方法添加@ApiOperation注解描述本地资源下载功能
- 创建ChannelInfo和ChannelInfoBase领域模型用于音频服务通道信息管理
- 新增CrtHelper工具类提供字节与十六进制字符串转换功能
- 新增DataCheckUtil工具类提供CRC16校验和时间转换功能
- 在firefighting-pressure-service中扩展DataCheckUtil添加时间转换功能
- 新增DataParser工具类用于音频数据解析和压力传感器数据解析
- 优化firefighting-pressure-service中开关量解析逻辑
- 为radar-service新增完整的雷达数据解析功能
林仔 1 mês atrás
pai
commit
7f066dbeba

+ 23 - 0
audio-service/src/main/java/com/zksy/audio/domain/ChannelInfo.java

@@ -0,0 +1,23 @@
+package com.zksy.audio.domain;
+
+import io.netty.channel.Channel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ChannelInfo extends ChannelInfoBase {
+
+    public ChannelInfo() {
+    }
+
+    public ChannelInfo(String deviceNo){
+        this.setDeviceNo(deviceNo);
+    }
+
+    public ChannelInfo(String meterFactory, Channel channel) {
+        this.setMeterFactory(meterFactory);
+        this.setChannel(channel);
+    }
+
+}

+ 33 - 0
audio-service/src/main/java/com/zksy/audio/domain/ChannelInfoBase.java

@@ -0,0 +1,33 @@
+package com.zksy.audio.domain;
+
+import io.netty.channel.Channel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class ChannelInfoBase {
+
+    private String meterFactory;
+
+    private String deviceNo;
+
+    private Channel channel;
+
+    private double power;
+
+    private double gain;
+
+    private String path;
+
+    private String filePath;
+
+    private double length;
+
+    private Date gatheTime;
+
+    private Boolean first;
+
+    private Long videoMaxSize;
+
+}

+ 42 - 0
audio-service/src/main/java/com/zksy/audio/utils/CrtHelper.java

@@ -0,0 +1,42 @@
+package com.zksy.audio.utils;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class CrtHelper {
+
+    public static String byteToHexStr1(byte[] bytes) {
+        String returnStr = "";
+        if (bytes != null) {
+            for (int i = 0; i < bytes.length; i++) {
+                returnStr += String.format("%02X", bytes[i]);
+
+            }
+        }
+        return returnStr;
+    }
+
+    public static byte[] strToToHexByte1(String hexString) {
+        hexString = hexString.replaceAll(" ", "");
+        if ((hexString.length() % 2) != 0) {
+            hexString += " ";
+        }
+        byte[] returnBytes = new byte[hexString.length() / 2];
+        for (int i = 0; i < returnBytes.length; i++) {
+            int pos = i * 2;
+
+            returnBytes[i] = (byte) Integer.parseInt(StrUtil.sub(hexString, pos, pos + 2), 16);
+        }
+        return returnBytes;
+    }
+
+    public static void main(String[] args) {
+        System.out.println(Arrays.stream(Arrays.toString(strToToHexByte1("242424323032343131303031300D0A")).split(",")).collect(Collectors.joining("")));
+    }
+
+}

+ 111 - 0
audio-service/src/main/java/com/zksy/audio/utils/DataCheckUtil.java

@@ -0,0 +1,111 @@
+package com.zksy.audio.utils;
+
+/**
+ * @Description
+ * @Date 2025- 03-06-上午 9:02
+ * @auther tuDouSi
+ */
+public class DataCheckUtil {
+    /**
+     *
+     * @description: crc16校验,输入一个数据,返回一个数组.
+     * @param str
+     * @return 返回两个校验码,低字节在前,高字节在后
+     *
+     */
+    public static String crc16(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda & 0xFF);
+        temdata[1] = (byte) (xda >> 8);
+        return bytesToHexString(temdata);
+    }
+    /**
+     *
+     * @description: crc16校验,
+     * @param str
+     * @return 返回两个校验码,高字节在前,低字节在后
+     *
+     */
+    public static String crc16Tall(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+
+        // 关键修改:先放高位字节,再放低位字节
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda >> 8);  // 高位字节
+        temdata[1] = (byte) (xda & 0xFF); // 低位字节
+
+        return bytesToHexString(temdata);
+    }
+    // 将十六进制字符串转换为整数数组
+    private static int[] hexStringToIntArray(String str) {
+        if (str.length() % 2 != 0) {
+            throw new IllegalArgumentException("Hex string length must be even");
+        }
+
+        int[] result = new int[str.length() / 2];
+        for (int i = 0; i < str.length(); i += 2) {
+            result[i / 2] = Integer.parseInt(str.substring(i, i + 2), 16);
+        }
+        return result;
+    }
+
+    // 将字节数组转换为十六进制字符串
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    public static int calcChecksum(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF);
+        }
+        return (0xFF - (sum & 0xFF)) & 0xFF;
+    }
+    /**
+     * 仅对数据累加后取低8位
+     * @param data 待计算字节数组
+     * @param offset 起始索引
+     * @param length 计算长度
+     * @return 8位校验结果
+     */
+    static int calcSumOnly(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF); // 累加每个字节的无符号值
+        }
+        return sum & 0xFF; // 仅取低8位,无FF-操作
+    }
+}

+ 159 - 0
audio-service/src/main/java/com/zksy/audio/utils/DataParser.java

@@ -0,0 +1,159 @@
+package com.zksy.audio.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.math3.complex.Complex;
+import org.apache.commons.math3.transform.DftNormalization;
+import org.apache.commons.math3.transform.FastFourierTransformer;
+import org.apache.commons.math3.transform.TransformType;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Slf4j
+public class DataParser {
+    public static short toInt16(byte[] buffer, boolean isBigEndian) {
+        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer)
+                .order(isBigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
+        return byteBuffer.getShort(0); // 第一个声道的数据在索引0处
+
+    }
+
+    public static int calculateRMS(String filePath) {
+        try {
+            File audioFile = new File(filePath);
+            AudioInputStream audioStream = AudioSystem.getAudioInputStream(audioFile);
+            AudioFormat format = audioStream.getFormat();
+            if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED && format.getSampleSizeInBits() != 16) {
+                throw new UnsupportedAudioFileException("Only 16-bit PCM signed WAV files are supported.");
+            }
+
+            int blockAlign = (format.getSampleSizeInBits() / 8) * format.getChannels();
+
+            int sampleCount = 0;
+            double sumOfSquares = 0;
+            double sum = 0;
+
+            byte[] rawBuffer = new byte[blockAlign];
+            int bytesRead;
+
+            while ((bytesRead = audioStream.read(rawBuffer, 0, rawBuffer.length)) > 0) {
+
+                for (int i = 0; i < bytesRead / blockAlign; i++) {
+
+                    int sample = toInt16(rawBuffer, format.isBigEndian());
+
+
+                    sum += sample;  // 累加样本值
+                    sampleCount++;
+                }
+
+            }
+
+            int mean = (int) (sum / sampleCount);
+
+            audioStream.close();
+            audioStream = AudioSystem.getAudioInputStream(audioFile);
+
+            sampleCount = 0;
+            sumOfSquares = 0;
+
+            // 从样本中减去平均值,计算新的平方和
+            while ((bytesRead = audioStream.read(rawBuffer, 0, rawBuffer.length)) > 0) {
+                for (int i = 0; i < bytesRead / blockAlign; i++) {
+                    int sample = toInt16(rawBuffer, format.isBigEndian());
+
+                    sample -= mean;  // 去掉平均值
+                    sumOfSquares += sample * sample;  // 累加平方值
+                    sampleCount++;
+                }
+            }
+
+            // 计算去直流后的 RMS
+            int rms = (int) Math.sqrt(sumOfSquares / sampleCount);
+
+            audioStream.close();
+
+            return rms;
+
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+        return 0;
+    }
+
+    public static short toInt16(byte[] array, int index) {
+        return ByteBuffer.wrap(array, index, 2)
+                .order(ByteOrder.LITTLE_ENDIAN)
+                .getShort();
+    }
+    public static double calcCenterFrequency(String wavFilePath) {
+        int fftSize = 1024;
+        double sampleRate = 8192; // 固定采样率
+        double centerFrequency = 0;
+        try (AudioInputStream audioStream = AudioSystem.getAudioInputStream(new File(wavFilePath))) {
+            AudioFormat format = audioStream.getFormat();
+
+            // 验证音频格式
+            if (format.getSampleRate() != 8192 || format.getSampleSizeInBits() != 16) {
+                throw new IllegalArgumentException("不支持的音频格式: 需要16位/8192Hz");
+            }
+
+            byte[] buffer = new byte[fftSize * 2];
+            float[] floatBuffer = new float[fftSize];
+            Complex[] fftBuffer = new Complex[fftSize];
+            double maxMagnitude = 0;
+            int maxIndex = 0;
+
+
+            // 初始化FFT变换器
+            FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD);
+
+            int bytesRead;
+            int counter = 0;
+            while ((bytesRead = audioStream.read(buffer)) > 0) {
+                if (bytesRead == 2048) {
+                    // 将字节转换为浮点数
+                    for (int i = 0; i < bytesRead / 2; i++) {
+                        //short sample = (short) ((buffer[i * 2 + 1] << 8) | (buffer[i * 2] & 0xFF));
+                        short sample = toInt16(buffer, i * 2);
+                        floatBuffer[i] = sample / 32768f;
+                        fftBuffer[i] = new Complex(floatBuffer[i], 0);
+                    }
+
+                    // 执行FFT变换
+                    Complex[] fftResult = fft.transform(fftBuffer, TransformType.FORWARD);
+                    counter++;
+
+                    // 计算幅度并寻找峰值
+                    maxMagnitude = 0;
+                    maxIndex = 0;
+                    for (int i = 0; i < fftSize / 2; i++) {
+                        double magnitude = Math.sqrt(fftResult[i].getReal() * fftResult[i].getReal()
+                                + fftResult[i].getImaginary() * fftResult[i].getImaginary());
+
+                        if (i > 0 && magnitude > maxMagnitude) {
+                            maxMagnitude = magnitude;
+                            maxIndex = i;
+                        }
+                    }
+
+                    // 计算中心频率
+                    double frequencyResolution = sampleRate / fftSize;
+                    centerFrequency = maxIndex * frequencyResolution;
+
+                }
+            }
+        } catch (UnsupportedAudioFileException | IOException e) {
+            throw new RuntimeException("音频处理错误", e);
+        }
+        return centerFrequency;
+    }
+}

+ 26 - 0
firefighting-pressure-service/src/main/java/com/zksy/pressure/utils/DataCheckUtil.java

@@ -1,5 +1,7 @@
 package com.zksy.pressure.utils;
 
+import java.time.LocalDateTime;
+
 /**
  * @Description
  * @Date 2025- 03-06-上午 9:02
@@ -86,4 +88,28 @@ public class DataCheckUtil {
         }
         return sb.toString();
     }
+    /**
+     * 获取当前时间的十六进制字符串(符合报文协议格式:年月日时分秒,每个部分两位十六进制)
+     * 示例:2025年09月11日16点35分07秒 → 17 01 06 11 35 07 → 拼接为 170106113507
+     * 转换规则:
+     * - 年:4位十进制 → 拆分为前两位+后两位,分别转十六进制(如2025 → 20=0x14,25=0x19 → 1419;2026→20=0x14,26=0x1A→141A)
+     * - 月/日/时/分/秒:2位十进制 → 直接转两位十六进制(不足两位补0)
+     * @return 12位的十六进制时间字符串(无空格)
+     */
+    static String getCurrentTimeHex() {
+        // 1. 获取当前本地时间
+        LocalDateTime now = LocalDateTime.now();
+
+        // 2. 拆分时间各部分(关键:年份只取后两位)
+        int yearLastTwo = now.getYear() % 100;  // 年的后两位
+        int month = now.getMonthValue();        // 月(1-12)
+        int day = now.getDayOfMonth();          // 日(1-31)
+        int hour = now.getHour();              // 时(0-23)
+        int minute = now.getMinute();          // 分(0-59)
+        int second = now.getSecond();          // 秒(0-59)
+        // %02d 表示:十进制、两位、不足补0
+        return String.format("%02d%02d%02d%02d%02d%02d",
+                yearLastTwo, month, day, hour, minute, second);
+    }
+
 }

+ 29 - 8
firefighting-pressure-service/src/main/java/com/zksy/pressure/utils/DataParser.java

@@ -36,10 +36,13 @@ public class DataParser {
             result.setCentralStation(getStringList(dataParts, 1));
             result.setTelemeteringStation(getStringList(dataParts, 5));
             result.setPassword(getStringList(dataParts, 2));
-            result.setFunctionCode(getStringList(dataParts, 1));
+            String functionCode = getStringList(dataParts, 1);
+            result.setFunctionCode(functionCode);
+            if("EF".equals(functionCode)){
+                return result;
+            }
             getStringList(dataParts, 3); // 长度+起始符
             result.setSerialNumber(getStringList(dataParts, 2));
-
             // 发报时间
             String sendTimeHex = getStringList(dataParts, 6);
             LocalDateTime sendTime = LocalDateTime.parse(sendTimeHex, formatterSend);
@@ -129,13 +132,31 @@ public class DataParser {
      * 解析开关量D31~D0
      */
     private static void parseSwitchStatus(FirefightingPressure result, String hex) {
-        // 小端模式
-        byte[] bytes = new BigInteger(hex, 16).toByteArray();
-        ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
-        buffer.put(bytes, Math.max(0, bytes.length - 4), Math.min(4, bytes.length));
-        buffer.flip();
-        long status = buffer.getInt() & 0xFFFFFFFFL;
+        if (hex == null || hex.length() != 8) {
+            logger.error("开关量格式错误,需要8位十六进制字符串,实际={}", hex);
+            return;
+        }
+
+        // 1. 将8位十六进制字符串转成4字节的小端整数
+        long status;
+        try {
+            // 先按大端解析成32位整数
+            int intValue = Integer.parseUnsignedInt(hex, 16);
+            // 再转换为小端字节序
+            byte[] bytes = ByteBuffer.allocate(4)
+                    .order(ByteOrder.BIG_ENDIAN)
+                    .putInt(intValue)
+                    .array();
+            // 小端模式读取
+            status = ByteBuffer.wrap(bytes)
+                    .order(ByteOrder.LITTLE_ENDIAN)
+                    .getInt() & 0xFFFFFFFFL;
+        } catch (NumberFormatException e) {
+            logger.error("开关量解析失败,hex={}", hex, e);
+            return;
+        }
 
+        // 2. 解析各个开关状态
         result.setD0((int) (status & 1));
         result.setD1((int) ((status >> 1) & 1));
         result.setD2((int) ((status >> 2) & 1));

+ 4 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/common/CaptchaController.java

@@ -9,6 +9,8 @@ import com.zksy.common.core.redis.RedisCache;
 import com.zksy.common.utils.sign.Base64;
 import com.zksy.common.utils.uuid.IdUtils;
 import com.zksy.system.service.ISysConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.FastByteArrayOutputStream;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -27,6 +29,7 @@ import java.util.concurrent.TimeUnit;
  * @author zksy
  */
 @RestController
+@Api(tags = "验证码")
 public class CaptchaController
 {
     @Resource(name = "captchaProducer")
@@ -44,6 +47,7 @@ public class CaptchaController
      * 生成验证码
      */
     @GetMapping("/captchaImage")
+    @ApiOperation(value = "生成验证码")
     public AjaxResult getCode(HttpServletResponse response) throws IOException
     {
         AjaxResult ajax = AjaxResult.success();

+ 7 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/common/CommonController.java

@@ -6,6 +6,8 @@ import com.zksy.common.utils.StringUtils;
 import com.zksy.common.utils.file.FileUploadUtils;
 import com.zksy.common.utils.file.FileUtils;
 import com.zksy.framework.config.ServerConfig;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -28,6 +30,7 @@ import java.util.List;
  */
 @RestController
 @RequestMapping("/common")
+@Api(tags = "通用接口")
 public class CommonController
 {
     private static final Logger log = LoggerFactory.getLogger(CommonController.class);
@@ -44,6 +47,7 @@ public class CommonController
      * @param delete 是否删除
      */
     @GetMapping("/download")
+    @ApiOperation(value = "通用下载请求")
     public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
     {
         try
@@ -73,6 +77,7 @@ public class CommonController
      * 通用上传请求(单个)
      */
     @PostMapping("/upload")
+    @ApiOperation(value = "通用上传请求(单个)")
     public AjaxResult uploadFile(MultipartFile file) throws Exception
     {
         try
@@ -99,6 +104,7 @@ public class CommonController
      * 通用上传请求(多个)
      */
     @PostMapping("/uploads")
+    @ApiOperation(value = "通用上传请求(多个)")
     public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
     {
         try
@@ -136,6 +142,7 @@ public class CommonController
      * 本地资源通用下载
      */
     @GetMapping("/download/resource")
+    @ApiOperation(value = "本地资源通用下载")
     public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
             throws Exception
     {

+ 10 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/monitor/CacheController.java

@@ -4,6 +4,8 @@ import com.zksy.common.constant.CacheConstants;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.utils.StringUtils;
 import com.zksy.system.domain.SysCache;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisCallback;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -19,6 +21,7 @@ import java.util.*;
  */
 @RestController
 @RequestMapping("/monitor/cache")
+@Api(tags = "缓存监控")
 public class CacheController
 {
     @Autowired
@@ -37,6 +40,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @GetMapping()
+    @ApiOperation("缓存列表")
     public AjaxResult getInfo() throws Exception
     {
         Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
@@ -61,6 +65,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @GetMapping("/getNames")
+    @ApiOperation("缓存名称列表")
     public AjaxResult cache()
     {
         return AjaxResult.success(caches);
@@ -68,6 +73,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @GetMapping("/getKeys/{cacheName}")
+    @ApiOperation("缓存键列表")
     public AjaxResult getCacheKeys(@PathVariable String cacheName)
     {
         Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
@@ -76,6 +82,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @GetMapping("/getValue/{cacheName}/{cacheKey}")
+    @ApiOperation("缓存值")
     public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
     {
         String cacheValue = redisTemplate.opsForValue().get(cacheKey);
@@ -85,6 +92,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @DeleteMapping("/clearCacheName/{cacheName}")
+    @ApiOperation("缓存名称")
     public AjaxResult clearCacheName(@PathVariable String cacheName)
     {
         Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
@@ -94,6 +102,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @DeleteMapping("/clearCacheKey/{cacheKey}")
+    @ApiOperation("缓存键")
     public AjaxResult clearCacheKey(@PathVariable String cacheKey)
     {
         redisTemplate.delete(cacheKey);
@@ -102,6 +111,7 @@ public class CacheController
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
     @DeleteMapping("/clearCacheAll")
+    @ApiOperation("缓存键")
     public AjaxResult clearCacheAll()
     {
         Collection<String> cacheKeys = redisTemplate.keys("*");

+ 111 - 0
radar-service/src/main/java/com/zksy/radar/utils/DataCheckUtil.java

@@ -0,0 +1,111 @@
+package com.zksy.radar.utils;
+
+/**
+ * @Description
+ * @Date 2025- 03-06-上午 9:02
+ * @auther tuDouSi
+ */
+public class DataCheckUtil {
+    /**
+     *
+     * @description: crc16校验,输入一个数据,返回一个数组.
+     * @param str
+     * @return 返回两个校验码,低字节在前,高字节在后
+     *
+     */
+    public static String crc16(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda & 0xFF);
+        temdata[1] = (byte) (xda >> 8);
+        return bytesToHexString(temdata);
+    }
+    /**
+     *
+     * @description: crc16校验,
+     * @param str
+     * @return 返回两个校验码,高字节在前,低字节在后
+     *
+     */
+    public static String crc16Tall(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+
+        // 关键修改:先放高位字节,再放低位字节
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda >> 8);  // 高位字节
+        temdata[1] = (byte) (xda & 0xFF); // 低位字节
+
+        return bytesToHexString(temdata);
+    }
+    // 将十六进制字符串转换为整数数组
+    private static int[] hexStringToIntArray(String str) {
+        if (str.length() % 2 != 0) {
+            throw new IllegalArgumentException("Hex string length must be even");
+        }
+
+        int[] result = new int[str.length() / 2];
+        for (int i = 0; i < str.length(); i += 2) {
+            result[i / 2] = Integer.parseInt(str.substring(i, i + 2), 16);
+        }
+        return result;
+    }
+
+    // 将字节数组转换为十六进制字符串
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    public static int calcChecksum(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF);
+        }
+        return (0xFF - (sum & 0xFF)) & 0xFF;
+    }
+    /**
+     * 仅对数据累加后取低8位
+     * @param data 待计算字节数组
+     * @param offset 起始索引
+     * @param length 计算长度
+     * @return 8位校验结果
+     */
+    static int calcSumOnly(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF); // 累加每个字节的无符号值
+        }
+        return sum & 0xFF; // 仅取低8位,无FF-操作
+    }
+}

+ 260 - 0
radar-service/src/main/java/com/zksy/radar/utils/DataParser.java

@@ -0,0 +1,260 @@
+package com.zksy.radar.utils;
+
+import com.zksy.common.exception.InvalidMessageException;
+import com.zksy.radar.domain.RadarData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+
+public class DataParser {
+
+    private static final Logger logger = LoggerFactory.getLogger(DataParser.class);
+
+    /**
+     * 平升6216协议完整校验:
+     * 1. 长度检查
+     * 2. 系统识别码检查
+     * 3. 异或校验(外层 PS 协议)
+     * 4. CRC16 校验(内层 历史记录协议)
+     */
+    public static void validateMessage(byte[] msgBytes) throws InvalidMessageException {
+        // 1. 长度检查
+        if (msgBytes == null || msgBytes.length < 10) {
+            throw new InvalidMessageException("数据帧长度过短");
+        }
+
+        // 2. 帧头与系统识别码检查
+        if (msgBytes[0] != 0x12 || msgBytes[1] != 0x34 || msgBytes[2] != 0x56) {
+            throw new InvalidMessageException("系统识别码错误,应为 12 34 56");
+        }
+
+        // 3. 校验整帧长度(从系统识别码到最后1字节)
+        int declaredLen = ((msgBytes[3] & 0xFF) << 8) | (msgBytes[4] & 0xFF);
+        if (declaredLen != msgBytes.length) {
+            throw new InvalidMessageException(String.format(
+                    "帧长度不匹配:声明=%d 实际=%d", declaredLen, msgBytes.length));
+        }
+
+        // 4. 异或校验(最后一字节)
+        byte receivedXor = msgBytes[msgBytes.length - 1];
+        byte[] xorData = Arrays.copyOfRange(msgBytes, 0, msgBytes.length - 1);
+        byte calculatedXor = ProtocolUtils.calculateXorCheck(xorData);
+        if (receivedXor != calculatedXor) {
+            throw new InvalidMessageException(String.format(
+                    "异或校验失败:计算=0x%02X 接收=0x%02X", calculatedXor, receivedXor));
+        }
+
+        // 5. 仅对上报历史记录帧(0x31)进行 CRC 校验
+        byte frameType = msgBytes[6];
+        if (frameType == 0x31) {
+            // CRC 高低字节在帧末尾的倒数第3、第2字节(倒数第1是异或)
+            int crcEndIndex = msgBytes.length - 3;
+            int crcHighIndex = crcEndIndex;
+            int crcLowIndex = crcEndIndex + 1;
+
+            if (crcHighIndex <= 0) {
+                throw new InvalidMessageException("CRC数据段索引错误");
+            }
+
+            // 设备编码开始(内层协议起点):
+            // 外层结构 3+2+1+1+1+n+1+m => 源地址/目的地址结束后紧接设备编码(0x01)
+            // 简化方式:找到第一个 0x01 2C 序列即可(设备编码 + 功能码)
+            int deviceCodeIndex = -1;
+            for (int i = 0; i < msgBytes.length - 1; i++) {
+                if (msgBytes[i] == 0x01 && msgBytes[i + 1] == 0x2C) {
+                    deviceCodeIndex = i;
+                    break;
+                }
+            }
+
+            if (deviceCodeIndex == -1) {
+                throw new InvalidMessageException("未找到设备编码起点 (0x01 2C)");
+            }
+
+            byte[] crcData = Arrays.copyOfRange(msgBytes, deviceCodeIndex, crcEndIndex);
+            int calculatedCrc = ProtocolUtils.calculateCRC16(crcData);
+            int receivedCrc = ((msgBytes[crcLowIndex] & 0xFF) << 8) | (msgBytes[crcHighIndex] & 0xFF);
+
+            if (calculatedCrc != receivedCrc) {
+                throw new InvalidMessageException(String.format(
+                        "CRC校验失败:计算=0x%04X 接收=0x%04X", calculatedCrc, receivedCrc));
+            }
+        }
+
+        logger.debug("消息通过所有校验");
+    }
+
+    /**
+     * 解析消息(支持历史记录帧0x31和结束通讯帧0x34,协议2.2.3)
+     */
+    public static RadarData parseMessage(byte[] msgBytes) {
+        RadarData data = new RadarData();
+        int index = 0;
+
+        // 1. 系统识别码
+        data.setSystemIdentifier(String.format("%02X%02X%02X", msgBytes[0], msgBytes[1], msgBytes[2]));
+        index += 3;
+
+        // 2. 帧长度
+        int frameLength = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+        data.setFrameLength(String.valueOf(frameLength));
+        index += 2;
+
+        // 3. 包序号、帧类型
+        data.setPacketSequence(String.format("%02X", msgBytes[index++]));
+        byte frameType = msgBytes[index++];
+        data.setDataType(String.format("0x%02X", frameType));
+
+        // 4. 源地址:按规则计算长度(原地址标识转10进制,奇数+1后除2)
+        int srcLenFlag = msgBytes[index++] & 0xFF; // 源地址标识(10进制)
+        int srcAddrSize = calculateAddrLength(srcLenFlag); // 计算源地址字节长度
+        srcAddrSize = Math.min(srcAddrSize, 10); // 防止越界,最多10字节
+        byte[] srcAddr = Arrays.copyOfRange(msgBytes, index, index + srcAddrSize);
+        data.setSourceAddr(parseBcdToStr(srcAddr));
+        index += srcAddrSize;
+
+        // 5. 目的地址:按相同规则计算长度
+        int dstLenFlag = msgBytes[index++] & 0xFF; // 目的地址标识(10进制)
+        int dstAddrSize = calculateAddrLength(dstLenFlag); // 计算目的地址字节长度
+        dstAddrSize = Math.min(dstAddrSize, 10); // 防止越界,最多10字节
+        byte[] dstAddr = Arrays.copyOfRange(msgBytes, index, index + dstAddrSize);
+        data.setDestAddr(parseBcdToStr(dstAddr));
+        data.setDestAddrLength(String.valueOf(dstAddrSize));
+        index += dstAddrSize;
+
+        // 6. 帧类型 = 0x31
+        if (frameType == 0x31) {
+            // 设备ID
+            data.setDeviceCode(String.format("%02X", msgBytes[index++]));
+            // 功能码
+            data.setFunctionCode(String.format("%02X", msgBytes[index++]));
+
+            // 预留6字节(原代码拆成了4+2,合并为6字节)
+            data.setReserve1(String.format("%02X%02X%02X%02X%02X%02X",
+                    msgBytes[index], msgBytes[index+1], msgBytes[index+2],
+                    msgBytes[index+3], msgBytes[index+4], msgBytes[index+5]));
+            index += 6;
+
+            // 记录数量
+            int recordCount = msgBytes[index++] & 0xFF;
+            data.setRecordCount(String.valueOf(recordCount));
+
+            // 记录格式
+            int recordFormat = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setRecordFormat(String.format("%04X", recordFormat));
+            index += 2;
+
+            // 电池电压(整型转浮点,除以100)
+            int powerVolt = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setPowerVoltage(String.format("%.2f", powerVolt / 100.0));
+            index += 2;
+
+            // 现场状态(解析D3位判断是否最后一包)
+            byte fieldStatus = msgBytes[index++];
+            data.setFieldStatus(String.format("%02X", fieldStatus));
+            // D3位:0x08 对应二进制 00001000,按位与判断
+            data.setIsLastPacket((fieldStatus & 0x08) != 0);
+
+            // 协议版本(两个字节)
+            data.setProtocolVersion(String.format("%02X", msgBytes[index++]));
+            data.setParamVersion(String.format("%02X", msgBytes[index++]));
+            // 信号质量
+            data.setSignalQuality(String.valueOf(msgBytes[index++] & 0xFF));
+
+            // 预留3字节
+            data.setReserve3(String.format("%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2]));
+            index += 3;
+
+            // --- 历史记录部分 ---
+            if (recordCount > 0 && index + 4 <= msgBytes.length) {
+                // 数据采集时间(4字节)
+                int ts = ((msgBytes[index] & 0xFF) << 24) | ((msgBytes[index + 1] & 0xFF) << 16)
+                        | ((msgBytes[index + 2] & 0xFF) << 8) | (msgBytes[index + 3] & 0xFF);
+                LocalDateTime recordTime = ProtocolUtils.convertToDateTime(ts);
+                data.setTimestampSince(recordTime);
+                index += 4;
+
+                // 表1净累计(4字节浮点)
+                float netTotal = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 表1瞬时流量(4字节浮点)
+                float instantFlow = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 流速(4字节浮点)
+                float flowSpeed = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 水位(4字节浮点)
+                float waterLevel = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 开关量(4字节整型)
+                long switchVal = bytesToUInt32(msgBytes, index);
+                index += 4;
+
+                // 设置浮点型数据(保留两位小数展示)
+                data.setMeter1NetTotal(String.format("%.2f", netTotal));
+                data.setMeter1InstantFlow(String.format("%.2f", instantFlow));
+                data.setFlowSpeed(String.format("%.2f", flowSpeed));
+                data.setWaterLevel(String.format("%.2f", waterLevel));
+                data.setSwitchValue(String.format("%08X", switchVal));
+
+                // 格式化历史记录字符串
+                data.setHistoryRecords(String.format(
+                        "时间:%s,净累计:%.2f,瞬时流量:%.2f,流速:%.2f,水位:%.2f,开关量:%08X",
+                        recordTime, netTotal, instantFlow, flowSpeed, waterLevel, switchVal));
+            }
+        }
+
+        data.setCreateTime(LocalDateTime.now());
+        return data;
+    }
+
+    /**
+     * 计算地址字节长度:原地址标识转10进制,奇数+1后除2
+     * @param lenFlag 地址标识(10进制值)
+     * @return 地址字节长度
+     */
+    private static int calculateAddrLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除2得到字节长度
+        return temp / 2;
+    }
+
+    /**
+     * 4字节(高字节在前)转单精度浮点型(IEEE 754)
+     * @param data 字节数组
+     * @param offset 起始偏移量
+     * @return 单精度浮点值
+     */
+    private static float bytesToFloat(byte[] data, int offset) {
+        // 先将4字节转为int(高字节在前)
+        int intValue = ((data[offset] & 0xFF) << 24)
+                | ((data[offset + 1] & 0xFF) << 16)
+                | ((data[offset + 2] & 0xFF) << 8)
+                | (data[offset + 3] & 0xFF);
+        // 按IEEE 754规则转换为float
+        return Float.intBitsToFloat(intValue);
+    }
+
+    /** 工具方法:BCD转字符串 */
+    private static String parseBcdToStr(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    /** 工具方法:4字节无符号整型 */
+    private static long bytesToUInt32(byte[] data, int offset) {
+        return ((long) (data[offset] & 0xFF) << 24)
+                | ((long) (data[offset + 1] & 0xFF) << 16)
+                | ((long) (data[offset + 2] & 0xFF) << 8)
+                | ((long) (data[offset + 3] & 0xFF));
+    }
+}

+ 93 - 26
telemetry-service/src/main/java/com/zksy/telemetry/utils/DataParser.java

@@ -85,6 +85,7 @@ public class DataParser {
 
         logger.debug("消息通过所有校验");
     }
+
     /**
      * 解析消息(支持历史记录帧0x31和结束通讯帧0x34,协议2.2.3)
      */
@@ -106,17 +107,18 @@ public class DataParser {
         byte frameType = msgBytes[index++];
         data.setDataType(String.format("0x%02X", frameType));
 
-        // 4. 源地址长度 + 内容
-        int srcLen = msgBytes[index++] & 0xFF;
-        // 实际有些设备固定6字节地址,防止越界
-        int srcAddrSize = Math.min(srcLen, 6);
+        // 4. 源地址:按规则计算长度(原地址标识转10进制,奇数+1后除2)
+        int srcLenFlag = msgBytes[index++] & 0xFF; // 源地址标识(10进制)
+        int srcAddrSize = calculateAddrLength(srcLenFlag); // 计算源地址字节长度
+        srcAddrSize = Math.min(srcAddrSize, 10); // 防止越界,最多10字节
         byte[] srcAddr = Arrays.copyOfRange(msgBytes, index, index + srcAddrSize);
         data.setSourceAddr(parseBcdToStr(srcAddr));
         index += srcAddrSize;
 
-        // 5. 目的地址长度 + 内容
-        int dstLen = msgBytes[index++] & 0xFF;
-        int dstAddrSize = Math.min(dstLen, 6);
+        // 5. 目的地址:按相同规则计算长度
+        int dstLenFlag = msgBytes[index++] & 0xFF; // 目的地址标识(10进制)
+        int dstAddrSize = calculateAddrLength(dstLenFlag); // 计算目的地址字节长度
+        dstAddrSize = Math.min(dstAddrSize, 10); // 防止越界,最多10字节
         byte[] dstAddr = Arrays.copyOfRange(msgBytes, index, index + dstAddrSize);
         data.setDestAddr(parseBcdToStr(dstAddr));
         data.setDestAddrLength(String.valueOf(dstAddrSize));
@@ -124,61 +126,95 @@ public class DataParser {
 
         // 6. 帧类型 = 0x31
         if (frameType == 0x31) {
+            // 设备ID
             data.setDeviceCode(String.format("%02X", msgBytes[index++]));
+            // 功能码
             data.setFunctionCode(String.format("%02X", msgBytes[index++]));
 
-            data.setReserve1(String.format("%02X%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2], msgBytes[index + 3]));
-            index += 4;
-            data.setReserve2(String.format("%02X%02X", msgBytes[index], msgBytes[index + 1]));
-            index += 2;
+            // 预留6字节(原代码拆成了4+2,合并为6字节)
+            data.setReserve1(String.format("%02X%02X%02X%02X%02X%02X",
+                    msgBytes[index], msgBytes[index+1], msgBytes[index+2],
+                    msgBytes[index+3], msgBytes[index+4], msgBytes[index+5]));
+            index += 6;
 
+            // 记录数量
             int recordCount = msgBytes[index++] & 0xFF;
             data.setRecordCount(String.valueOf(recordCount));
 
+            // 记录格式
             int recordFormat = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
             data.setRecordFormat(String.format("%04X", recordFormat));
             index += 2;
 
+            // 电池电压(整型转浮点,除以100)
             int powerVolt = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
             data.setPowerVoltage(String.format("%.2f", powerVolt / 100.0));
             index += 2;
 
+            // 现场状态(解析D3位判断是否最后一包)
             byte fieldStatus = msgBytes[index++];
             data.setFieldStatus(String.format("%02X", fieldStatus));
+            // D3位:0x08 对应二进制 00001000,按位与判断
             data.setIsLastPacket((fieldStatus & 0x08) != 0);
 
+            // 协议版本(两个字节)
             data.setProtocolVersion(String.format("%02X", msgBytes[index++]));
             data.setParamVersion(String.format("%02X", msgBytes[index++]));
+            // 信号质量
             data.setSignalQuality(String.valueOf(msgBytes[index++] & 0xFF));
 
+            // 预留3字节
             data.setReserve3(String.format("%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2]));
             index += 3;
 
             // --- 历史记录部分 ---
             if (recordCount > 0 && index + 4 <= msgBytes.length) {
+                // 数据采集时间(4字节)
                 int ts = ((msgBytes[index] & 0xFF) << 24) | ((msgBytes[index + 1] & 0xFF) << 16)
                         | ((msgBytes[index + 2] & 0xFF) << 8) | (msgBytes[index + 3] & 0xFF);
                 LocalDateTime recordTime = ProtocolUtils.convertToDateTime(ts);
                 data.setTimestampSince(recordTime);
                 index += 4;
 
-                long net = bytesToUInt32(msgBytes, index); index += 4;
-                long pos = bytesToUInt32(msgBytes, index); index += 4;
-                long neg = bytesToUInt32(msgBytes, index); index += 4;
-                long flow = bytesToUInt32(msgBytes, index); index += 4;
-                long pressure = bytesToUInt32(msgBytes, index); index += 4;
-                long switchVal = bytesToUInt32(msgBytes, index); index += 4;
-
-                data.setMeter1NetTotal(String.valueOf(net));
-                data.setMeter1PositiveTotal(String.valueOf(pos));
-                data.setMeter1NegativeTotal(String.valueOf(neg));
-                data.setMeter1InstantFlow(String.valueOf(flow));
-                data.setPressure(String.valueOf(pressure));
+                // 表1净累计(4字节浮点)
+                float netTotal = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 表1正累计(4字节浮点)
+                float posTotal = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 表1负累计(4字节浮点)
+                float negTotal = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 表1瞬时流量(4字节浮点)
+                float instantFlow = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 流速(4字节浮点)
+                float flowSpeed = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 压力(4字节浮点)
+                float pressure = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 温度(4字节浮点)
+                float temperature = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 开关量(4字节整型)
+                long switchVal = bytesToUInt32(msgBytes, index);
+                index += 4;
+
+                // 设置浮点型数据(保留两位小数展示)
+                data.setMeter1NetTotal(String.format("%.2f", netTotal));
+                data.setMeter1PositiveTotal(String.format("%.2f", posTotal));
+                data.setMeter1NegativeTotal(String.format("%.2f", negTotal));
+                data.setMeter1InstantFlow(String.format("%.2f", instantFlow));
+                data.setFlowSpeed(String.format("%.2f", flowSpeed)); // 新增流速字段
+                data.setPressure(String.format("%.2f", pressure));
+                data.setTemperature(String.format("%.2f", temperature)); // 新增温度字段
                 data.setSwitchValue(String.format("%08X", switchVal));
 
+                // 格式化历史记录字符串
                 data.setHistoryRecords(String.format(
-                        "时间:%s,净累计:%d,正累计:%d,负累计:%d,瞬时流量:%d,压力:%d,开关量:%08X",
-                        recordTime, net, pos, neg, flow, pressure, switchVal));
+                        "时间:%s,净累计:%.2f,正累计:%.2f,负累计:%.2f,瞬时流量:%.2f,流速:%.2f,压力:%.2f,温度:%.2f,开关量:%08X",
+                        recordTime, netTotal, posTotal, negTotal, instantFlow, flowSpeed, pressure, temperature, switchVal));
             }
         }
 
@@ -186,6 +222,37 @@ public class DataParser {
         return data;
     }
 
+    /**
+     * 计算地址字节长度:原地址标识转10进制,奇数+1后除2
+     * @param lenFlag 地址标识(10进制值)
+     * @return 地址字节长度
+     */
+    private static int calculateAddrLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除2得到字节长度
+        return temp / 2;
+    }
+
+    /**
+     * 4字节(高字节在前)转单精度浮点型(IEEE 754)
+     * @param data 字节数组
+     * @param offset 起始偏移量
+     * @return 单精度浮点值
+     */
+    private static float bytesToFloat(byte[] data, int offset) {
+        // 先将4字节转为int(高字节在前)
+        int intValue = ((data[offset] & 0xFF) << 24)
+                | ((data[offset + 1] & 0xFF) << 16)
+                | ((data[offset + 2] & 0xFF) << 8)
+                | (data[offset + 3] & 0xFF);
+        // 按IEEE 754规则转换为float
+        return Float.intBitsToFloat(intValue);
+    }
+
     /** 工具方法:BCD转字符串 */
     private static String parseBcdToStr(byte[] bytes) {
         StringBuilder sb = new StringBuilder();
@@ -202,4 +269,4 @@ public class DataParser {
                 | ((long) (data[offset + 2] & 0xFF) << 8)
                 | ((long) (data[offset + 3] & 0xFF));
     }
-}
+}

+ 111 - 0
water-level-service/src/main/java/com/zksy/water/utils/DataCheckUtil.java

@@ -0,0 +1,111 @@
+package com.zksy.water.utils;
+
+/**
+ * @Description
+ * @Date 2025- 03-06-上午 9:02
+ * @auther tuDouSi
+ */
+public class DataCheckUtil {
+    /**
+     *
+     * @description: crc16校验,输入一个数据,返回一个数组.
+     * @param str
+     * @return 返回两个校验码,低字节在前,高字节在后
+     *
+     */
+    public static String crc16(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda & 0xFF);
+        temdata[1] = (byte) (xda >> 8);
+        return bytesToHexString(temdata);
+    }
+    /**
+     *
+     * @description: crc16校验,
+     * @param str
+     * @return 返回两个校验码,高字节在前,低字节在后
+     *
+     */
+    public static String crc16Tall(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+
+        // 关键修改:先放高位字节,再放低位字节
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda >> 8);  // 高位字节
+        temdata[1] = (byte) (xda & 0xFF); // 低位字节
+
+        return bytesToHexString(temdata);
+    }
+    // 将十六进制字符串转换为整数数组
+    private static int[] hexStringToIntArray(String str) {
+        if (str.length() % 2 != 0) {
+            throw new IllegalArgumentException("Hex string length must be even");
+        }
+
+        int[] result = new int[str.length() / 2];
+        for (int i = 0; i < str.length(); i += 2) {
+            result[i / 2] = Integer.parseInt(str.substring(i, i + 2), 16);
+        }
+        return result;
+    }
+
+    // 将字节数组转换为十六进制字符串
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    public static int calcChecksum(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF);
+        }
+        return (0xFF - (sum & 0xFF)) & 0xFF;
+    }
+    /**
+     * 仅对数据累加后取低8位
+     * @param data 待计算字节数组
+     * @param offset 起始索引
+     * @param length 计算长度
+     * @return 8位校验结果
+     */
+    static int calcSumOnly(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF); // 累加每个字节的无符号值
+        }
+        return sum & 0xFF; // 仅取低8位,无FF-操作
+    }
+}

+ 247 - 0
water-level-service/src/main/java/com/zksy/water/utils/DataParser.java

@@ -0,0 +1,247 @@
+package com.zksy.water.utils;
+
+import com.zksy.common.exception.InvalidMessageException;
+import com.zksy.water.domain.WaterMonitorData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+
+public class DataParser {
+
+    private static final Logger logger = LoggerFactory.getLogger(DataParser.class);
+
+    /**
+     * 平升6216协议完整校验:
+     * 1. 长度检查
+     * 2. 系统识别码检查
+     * 3. 异或校验(外层 PS 协议)
+     * 4. CRC16 校验(内层 历史记录协议)
+     */
+    public static void validateMessage(byte[] msgBytes) throws InvalidMessageException {
+        // 1. 长度检查
+        if (msgBytes == null || msgBytes.length < 10) {
+            throw new InvalidMessageException("数据帧长度过短");
+        }
+
+        // 2. 帧头与系统识别码检查
+        if (msgBytes[0] != 0x12 || msgBytes[1] != 0x34 || msgBytes[2] != 0x56) {
+            throw new InvalidMessageException("系统识别码错误,应为 12 34 56");
+        }
+
+        // 3. 校验整帧长度(从系统识别码到最后1字节)
+        int declaredLen = ((msgBytes[3] & 0xFF) << 8) | (msgBytes[4] & 0xFF);
+        if (declaredLen != msgBytes.length) {
+            throw new InvalidMessageException(String.format(
+                    "帧长度不匹配:声明=%d 实际=%d", declaredLen, msgBytes.length));
+        }
+
+        // 4. 异或校验(最后一字节)
+        byte receivedXor = msgBytes[msgBytes.length - 1];
+        byte[] xorData = Arrays.copyOfRange(msgBytes, 0, msgBytes.length - 1);
+        byte calculatedXor = ProtocolUtils.calculateXorCheck(xorData);
+        if (receivedXor != calculatedXor) {
+            throw new InvalidMessageException(String.format(
+                    "异或校验失败:计算=0x%02X 接收=0x%02X", calculatedXor, receivedXor));
+        }
+
+        // 5. 仅对上报历史记录帧(0x31)进行 CRC 校验
+        byte frameType = msgBytes[6];
+        if (frameType == 0x31) {
+            // CRC 高低字节在帧末尾的倒数第3、第2字节(倒数第1是异或)
+            int crcEndIndex = msgBytes.length - 3;
+            int crcHighIndex = crcEndIndex;
+            int crcLowIndex = crcEndIndex + 1;
+
+            if (crcHighIndex <= 0) {
+                throw new InvalidMessageException("CRC数据段索引错误");
+            }
+
+            // 设备编码开始(内层协议起点):
+            // 外层结构 3+2+1+1+1+n+1+m => 源地址/目的地址结束后紧接设备编码(0x01)
+            // 简化方式:找到第一个 0x01 2C 序列即可(设备编码 + 功能码)
+            int deviceCodeIndex = -1;
+            for (int i = 0; i < msgBytes.length - 1; i++) {
+                if (msgBytes[i] == 0x01 && msgBytes[i + 1] == 0x2C) {
+                    deviceCodeIndex = i;
+                    break;
+                }
+            }
+
+            if (deviceCodeIndex == -1) {
+                throw new InvalidMessageException("未找到设备编码起点 (0x01 2C)");
+            }
+
+            byte[] crcData = Arrays.copyOfRange(msgBytes, deviceCodeIndex, crcEndIndex);
+            int calculatedCrc = ProtocolUtils.calculateCRC16(crcData);
+            int receivedCrc = ((msgBytes[crcLowIndex] & 0xFF) << 8) | (msgBytes[crcHighIndex] & 0xFF);
+
+            if (calculatedCrc != receivedCrc) {
+                throw new InvalidMessageException(String.format(
+                        "CRC校验失败:计算=0x%04X 接收=0x%04X", calculatedCrc, receivedCrc));
+            }
+        }
+
+        logger.debug("消息通过所有校验");
+    }
+
+    /**
+     * 解析消息(支持历史记录帧0x31和结束通讯帧0x34,协议2.2.3)
+     */
+    public static WaterMonitorData parseMessage(byte[] msgBytes) {
+        WaterMonitorData data = new WaterMonitorData();
+        int index = 0;
+
+        // 1. 系统识别码
+        data.setSystemIdentifier(String.format("%02X%02X%02X", msgBytes[0], msgBytes[1], msgBytes[2]));
+        index += 3;
+
+        // 2. 帧长度
+        int frameLength = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+        data.setFrameLength(String.valueOf(frameLength));
+        index += 2;
+
+        // 3. 包序号、帧类型
+        data.setPacketSequence(String.format("%02X", msgBytes[index++]));
+        byte frameType = msgBytes[index++];
+        data.setDataType(String.format("0x%02X", frameType));
+
+        // 4. 源地址:按规则计算长度(原地址标识转10进制,奇数+1后除2)
+        int srcLenFlag = msgBytes[index++] & 0xFF; // 源地址标识(10进制)
+        int srcAddrSize = calculateAddrLength(srcLenFlag); // 计算源地址字节长度
+        srcAddrSize = Math.min(srcAddrSize, 10); // 防止越界,最多10字节
+        byte[] srcAddr = Arrays.copyOfRange(msgBytes, index, index + srcAddrSize);
+        data.setSourceAddr(parseBcdToStr(srcAddr));
+        index += srcAddrSize;
+
+        // 5. 目的地址:按相同规则计算长度
+        int dstLenFlag = msgBytes[index++] & 0xFF; // 目的地址标识(10进制)
+        int dstAddrSize = calculateAddrLength(dstLenFlag); // 计算目的地址字节长度
+        dstAddrSize = Math.min(dstAddrSize, 10); // 防止越界,最多10字节
+        byte[] dstAddr = Arrays.copyOfRange(msgBytes, index, index + dstAddrSize);
+        data.setDestAddr(parseBcdToStr(dstAddr));
+        data.setDestAddrLength(String.valueOf(dstAddrSize));
+        index += dstAddrSize;
+
+        // 6. 帧类型 = 0x31
+        if (frameType == 0x31) {
+            // 设备ID
+            data.setDeviceCode(String.format("%02X", msgBytes[index++]));
+            // 功能码
+            data.setFunctionCode(String.format("%02X", msgBytes[index++]));
+
+            // 预留6字节(原代码拆成了4+2,合并为6字节)
+            data.setReserve1(String.format("%02X%02X%02X%02X%02X%02X",
+                    msgBytes[index], msgBytes[index+1], msgBytes[index+2],
+                    msgBytes[index+3], msgBytes[index+4], msgBytes[index+5]));
+            index += 6;
+
+            // 记录数量
+            int recordCount = msgBytes[index++] & 0xFF;
+            data.setRecordCount(String.valueOf(recordCount));
+
+            // 记录格式
+            int recordFormat = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setRecordFormat(String.format("%04X", recordFormat));
+            index += 2;
+
+            // 电池电压(整型转浮点,除以100)
+            int powerVolt = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setPowerVoltage(String.format("%.2f", powerVolt / 100.0));
+            index += 2;
+
+            // 现场状态(解析D3位判断是否最后一包)
+            byte fieldStatus = msgBytes[index++];
+            data.setFieldStatus(String.format("%02X", fieldStatus));
+            // D3位:0x08 对应二进制 00001000,按位与判断
+            data.setIsLastPacket((fieldStatus & 0x08) != 0);
+
+            // 协议版本(两个字节)
+            data.setProtocolVersion(String.format("%02X", msgBytes[index++]));
+            data.setParamVersion(String.format("%02X", msgBytes[index++]));
+            // 信号质量
+            data.setSignalQuality(String.valueOf(msgBytes[index++] & 0xFF));
+
+            // 预留3字节
+            data.setReserve3(String.format("%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2]));
+            index += 3;
+
+            // --- 历史记录部分 ---
+            if (recordCount > 0 && index + 4 <= msgBytes.length) {
+                // 数据采集时间(4字节)
+                int ts = ((msgBytes[index] & 0xFF) << 24) | ((msgBytes[index + 1] & 0xFF) << 16)
+                        | ((msgBytes[index + 2] & 0xFF) << 8) | (msgBytes[index + 3] & 0xFF);
+                LocalDateTime recordTime = ProtocolUtils.convertToDateTime(ts);
+                data.setTimestampSince(recordTime);
+                index += 4;
+                // 水位(4字节浮点)
+                float waterLevel = bytesToFloat(msgBytes, index);
+                index += 4;
+                // 开关量(4字节整型)
+                long switchVal = bytesToUInt32(msgBytes, index);
+                index += 4;
+
+                // 设置浮点型数据(保留两位小数展示)
+                data.setWaterLevel(String.format("%.2f", waterLevel));
+                data.setSwitchValue(String.format("%08X", switchVal));
+
+                // 格式化历史记录字符串
+                data.setHistoryRecords(String.format(
+                        "时间:%s,水位:%.2f,开关量:%08X",
+                        recordTime,waterLevel, switchVal));
+            }
+        }
+
+        data.setCreateTime(LocalDateTime.now());
+        return data;
+    }
+
+    /**
+     * 计算地址字节长度:原地址标识转10进制,奇数+1后除2
+     * @param lenFlag 地址标识(10进制值)
+     * @return 地址字节长度
+     */
+    private static int calculateAddrLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除2得到字节长度
+        return temp / 2;
+    }
+
+    /**
+     * 4字节(高字节在前)转单精度浮点型(IEEE 754)
+     * @param data 字节数组
+     * @param offset 起始偏移量
+     * @return 单精度浮点值
+     */
+    private static float bytesToFloat(byte[] data, int offset) {
+        // 先将4字节转为int(高字节在前)
+        int intValue = ((data[offset] & 0xFF) << 24)
+                | ((data[offset + 1] & 0xFF) << 16)
+                | ((data[offset + 2] & 0xFF) << 8)
+                | (data[offset + 3] & 0xFF);
+        // 按IEEE 754规则转换为float
+        return Float.intBitsToFloat(intValue);
+    }
+
+    /** 工具方法:BCD转字符串 */
+    private static String parseBcdToStr(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    /** 工具方法:4字节无符号整型 */
+    private static long bytesToUInt32(byte[] data, int offset) {
+        return ((long) (data[offset] & 0xFF) << 24)
+                | ((long) (data[offset + 1] & 0xFF) << 16)
+                | ((long) (data[offset + 2] & 0xFF) << 8)
+                | ((long) (data[offset + 3] & 0xFF));
+    }
+}