20260126:模型排序&高德地图

This commit is contained in:
George 2026-01-26 19:17:47 +08:00
parent 652a32b797
commit b770da12e3
13 changed files with 5348 additions and 5153 deletions

View File

@ -102,7 +102,7 @@ spring:
redis: redis:
host: localhost # 地址 host: localhost # 地址
port: 6379 # 端口,默认为6379 port: 6379 # 端口,默认为6379
database: 3 # 数据库索引 database: 0 # 数据库索引
password: fastbee # 密码 password: fastbee # 密码
timeout: 10s # 连接超时时间 timeout: 10s # 连接超时时间
lettuce: lettuce:

View File

@ -50,8 +50,11 @@ public class TCPBootStrap {
.setPort(port) .setPort(port)
//设置报文最大长度 modbus-rtu //设置报文最大长度 modbus-rtu
.setMaxFrameLength(1024) .setMaxFrameLength(1024)
// 此处就是ASCII和注册包粘包了,无需strip掉 “分隔符” 即可 /*
.setDelimiters(new Delimiter(new byte[]{0x7e}, false)) * 此处就是ASCII和注册包粘包了,无需strip掉 “分隔符” 即可
* 最原始.setDelimiters(new Delimiter(new byte[]{0x7e}, true))
*/
.setDelimiters(new Delimiter(new byte[]{0x0A}, true))
.setDecoder(messageAdapter) .setDecoder(messageAdapter)
.setEncoder(messageAdapter) .setEncoder(messageAdapter)
.setHandlerMapping(handlerMapping) .setHandlerMapping(handlerMapping)

View File

@ -125,13 +125,12 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
if (minDelim != null) { if (minDelim != null) {
int minDelimLength = minDelim.value.length; int minDelimLength = minDelim.value.length;
ByteBuf frame = null; ByteBuf frame = null;
boolean keepDelimiterForNextFrame = shouldKeepDelimiterForNextFrame(minDelim) && minFrameLength > 0;
if (discardingTooLongFrame) { if (discardingTooLongFrame) {
// 如果true,将长度不符合报文丢弃 // 如果true,将长度不符合报文丢弃
// 初始化原来的值 // 初始化原来的值
discardingTooLongFrame = false; discardingTooLongFrame = false;
buffer.skipBytes(minFrameLength + (keepDelimiterForNextFrame ? 0 : minDelimLength)); buffer.skipBytes(minFrameLength + minDelimLength);
int tooLongFrameLength = this.tooLongFrameLength; int tooLongFrameLength = this.tooLongFrameLength;
this.tooLongFrameLength = 0; this.tooLongFrameLength = 0;
@ -143,16 +142,10 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
/*小于最小长度帧处理*/ /*小于最小长度帧处理*/
if (minFrameLength > maxFrameLength) { if (minFrameLength > maxFrameLength) {
//放弃读取帧 //放弃读取帧
buffer.skipBytes(minFrameLength + (keepDelimiterForNextFrame ? 0 : minDelimLength)); buffer.skipBytes(minFrameLength + minDelimLength);
fail(minFrameLength); fail(minFrameLength);
return null; return null;
} }
if (keepDelimiterForNextFrame) {
frame = buffer.readRetainedSlice(minFrameLength);
return frame;
}
/*是否需要跳过某字节*/ /*是否需要跳过某字节*/
if (minDelim.strip) { if (minDelim.strip) {
//忽略长度等于0的报文 //忽略长度等于0的报文
@ -273,9 +266,6 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
return -1; return -1;
} }
// Modbus RTU:为了避免把请求帧(常见固定8字节)误当响应帧(第3字节是byteCount)导致截断,
// 这里采用"候选长度 + CRC校验"方式确定帧边界,兼容:请求/回显/响应/异常响应。
// 异常响应:地址(1) + 功能码(1|0x80) + 异常码(1) + CRC(2) = 5字节 // 异常响应:地址(1) + 功能码(1|0x80) + 异常码(1) + CRC(2) = 5字节
if ((functionCode & 0x80) != 0) { if ((functionCode & 0x80) != 0) {
int len = 5; int len = 5;
@ -309,8 +299,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
} }
int byteCount = buffer.getByte(readerIndex + 2) & 0xFF; int byteCount = buffer.getByte(readerIndex + 2) & 0xFF;
int respLen = 3 + byteCount + 2; int respLen = 3 + byteCount + 2;
log.debug("[{}] Modbus功能码{}: byteCount={}, 预期帧长={}, 可读={}", log.debug("[{}] Modbus功能码{}: byteCount={}, 预期帧长={}, 可读={}", deviceId, String.format("%02X", functionCode), byteCount, respLen, readable);
deviceId, String.format("%02X", functionCode), byteCount, respLen, readable);
if (respLen > maxFrameLength) { if (respLen > maxFrameLength) {
log.warn("[{}] Modbus帧长度{}超过最大限制{}", deviceId, respLen, maxFrameLength); log.warn("[{}] Modbus帧长度{}超过最大限制{}", deviceId, respLen, maxFrameLength);
return -1; return -1;
@ -368,13 +357,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
byte[] crc = CRC16Utils.getCrc16Byte(data); // [low, high] byte[] crc = CRC16Utils.getCrc16Byte(data); // [low, high]
int crcLo = buffer.getByte(offset + len - 2) & 0xFF; int crcLo = buffer.getByte(offset + len - 2) & 0xFF;
int crcHi = buffer.getByte(offset + len - 1) & 0xFF; int crcHi = buffer.getByte(offset + len - 1) & 0xFF;
boolean valid = ((crc[0] & 0xFF) == crcLo) && ((crc[1] & 0xFF) == crcHi); return ((crc[0] & 0xFF) == crcLo) && ((crc[1] & 0xFF) == crcHi);
if (!valid && log.isDebugEnabled()) {
log.debug("[{}] CRC校验: 计算值={}{}, 实际值={}{}", deviceId,
String.format("%02X", crc[0] & 0xFF), String.format("%02X", crc[1] & 0xFF),
String.format("%02X", crcLo), String.format("%02X", crcHi));
}
return valid;
} }
private void fail(long frameLength) { private void fail(long frameLength) {

View File

@ -114,4 +114,10 @@ public class ThingsModelValueItem {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date ts; private Date ts;
/**
* 创建时间(用于排序)
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
} }

View File

@ -599,6 +599,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
arrayObjectThingsModel.setIsReadonly(param.getIsReadonly()); arrayObjectThingsModel.setIsReadonly(param.getIsReadonly());
arrayObjectThingsModel.setDatatype(param.getDatatype()); arrayObjectThingsModel.setDatatype(param.getDatatype());
arrayObjectThingsModel.setModelOrder(thingsModel.getModelOrder()); arrayObjectThingsModel.setModelOrder(thingsModel.getModelOrder());
arrayObjectThingsModel.setCreateTime(thingsModel.getCreateTime());
resultList.add(arrayObjectThingsModel); resultList.add(arrayObjectThingsModel);
} }
} }
@ -617,6 +618,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
arrayThingsModel.setIsReadonly(thingsModel.getIsReadonly()); arrayThingsModel.setIsReadonly(thingsModel.getIsReadonly());
arrayThingsModel.setDatatype(datatype); arrayThingsModel.setDatatype(datatype);
arrayThingsModel.setModelOrder(thingsModel.getModelOrder()); arrayThingsModel.setModelOrder(thingsModel.getModelOrder());
arrayThingsModel.setCreateTime(thingsModel.getCreateTime());
resultList.add(arrayThingsModel); resultList.add(arrayThingsModel);
} }
} else if ("object".equals(datatype.getType())) { } else if ("object".equals(datatype.getType())) {
@ -630,6 +632,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
newObjectThingsModel.setIsReadonly(objectThingsModel.getIsReadonly()); newObjectThingsModel.setIsReadonly(objectThingsModel.getIsReadonly());
newObjectThingsModel.setDatatype(objectThingsModel.getDatatype()); newObjectThingsModel.setDatatype(objectThingsModel.getDatatype());
newObjectThingsModel.setModelOrder(thingsModel.getModelOrder()); newObjectThingsModel.setModelOrder(thingsModel.getModelOrder());
newObjectThingsModel.setCreateTime(thingsModel.getCreateTime());
resultList.add(newObjectThingsModel); resultList.add(newObjectThingsModel);
} }
} else { } else {
@ -642,6 +645,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
otherThingsModel.setIsReadonly(thingsModel.getIsReadonly()); otherThingsModel.setIsReadonly(thingsModel.getIsReadonly());
otherThingsModel.setDatatype(datatype); otherThingsModel.setDatatype(datatype);
otherThingsModel.setModelOrder(thingsModel.getModelOrder()); otherThingsModel.setModelOrder(thingsModel.getModelOrder());
otherThingsModel.setCreateTime(thingsModel.getCreateTime());
resultList.add(otherThingsModel); resultList.add(otherThingsModel);
} }
return resultList; return resultList;
@ -686,24 +690,31 @@ public class ThingsModelServiceImpl implements IThingsModelService {
if (null == productId) { if (null == productId) {
return new ArrayList<>(); return new ArrayList<>();
} }
// 1、获取产品物模型 // 排序器:按 modelOrder 降序,createTime 降序(与数据库 SQL 一致)
Comparator<ThingsModelDTO> modelComparator = Comparator
.comparing(ThingsModelDTO::getModelOrder, Comparator.reverseOrder())
.thenComparing(ThingsModelDTO::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder()));
// 1、获取产品物模型并排序
Map<String, ThingsModelValueItem> map = itslCache.getCacheThMapByProductId(productId); Map<String, ThingsModelValueItem> map = itslCache.getCacheThMapByProductId(productId);
List<ThingsModelDTO> thingsModelDTOList = new ArrayList<>(); List<ThingsModelDTO> thingsModelDTOList = new ArrayList<>();
for (Map.Entry<String, ThingsModelValueItem> entry : map.entrySet()) { for (Map.Entry<String, ThingsModelValueItem> entry : map.entrySet()) {
ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue()); ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue());
thingsModelDTOList.addAll(this.changeObjectOrArray(thingsModel)); thingsModelDTOList.addAll(this.changeObjectOrArray(thingsModel));
} }
thingsModelDTOList.sort(modelComparator);
// 2. 获取设备级计算物模型并合并 // 2. 获取设备级计算物模型并合并
if (StringUtils.isNotEmpty(serialNumber)) { if (StringUtils.isNotEmpty(serialNumber)) {
Map<String, ThingsModelValueItem> deviceCalculatedMap = itslCache.getDeviceCalculatedModelMap(serialNumber); Map<String, ThingsModelValueItem> deviceCalculatedMap = itslCache.getDeviceCalculatedModelMap(serialNumber);
List<ThingsModelDTO> deviceModelList = new ArrayList<>();
for (Map.Entry<String, ThingsModelValueItem> entry : deviceCalculatedMap.entrySet()) { for (Map.Entry<String, ThingsModelValueItem> entry : deviceCalculatedMap.entrySet()) {
ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue()); ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue());
thingsModelDTOList.addAll(this.changeObjectOrArray(thingsModel)); deviceModelList.addAll(this.changeObjectOrArray(thingsModel));
} }
deviceModelList.sort(modelComparator);
thingsModelDTOList.addAll(deviceModelList);
} }
thingsModelDTOList.sort(Comparator.comparing(ThingsModelDTO::getModelOrder));
if (StringUtils.isEmpty(serialNumber)) { if (StringUtils.isEmpty(serialNumber)) {
return thingsModelDTOList; return thingsModelDTOList;
} }
@ -767,6 +778,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
thingsModel.setSpecs(JSON.toJSONString(valueItem.getDatatype())); thingsModel.setSpecs(JSON.toJSONString(valueItem.getDatatype()));
thingsModel.setType(valueItem.getType()); thingsModel.setType(valueItem.getType());
thingsModel.setModelId(valueItem.getModelId()); thingsModel.setModelId(valueItem.getModelId());
thingsModel.setCreateTime(valueItem.getCreateTime());
return thingsModel; return thingsModel;
} }

14
vue/package-lock.json generated
View File

@ -5751,6 +5751,15 @@
"zrender": "5.6.1" "zrender": "5.6.1"
} }
}, },
"echarts-extension-leaflet": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/echarts-extension-leaflet/-/echarts-extension-leaflet-1.2.2.tgz",
"integrity": "sha512-EKZmIQDRqxnf6DtYWqujn2PRqHmEAq3yr0gsZAwmOT9rSy0Rwt9FlXKPjK3d+EVLqT6se6IQweyJTsqMg537Gg==",
"requires": {
"echarts": "^5.5.0",
"leaflet": "^1.9.4"
}
},
"echarts-liquidfill": { "echarts-liquidfill": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/echarts-liquidfill/-/echarts-liquidfill-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/echarts-liquidfill/-/echarts-liquidfill-3.1.0.tgz",
@ -8974,6 +8983,11 @@
"launch-editor": "^2.12.0" "launch-editor": "^2.12.0"
} }
}, },
"leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmmirror.com/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"leven": { "leven": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/leven/-/leven-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/leven/-/leven-2.1.0.tgz",

View File

@ -46,6 +46,7 @@
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"core-js": "3.25.3", "core-js": "3.25.3",
"echarts": "^5.4.0", "echarts": "^5.4.0",
"echarts-extension-leaflet": "^1.2.2",
"echarts-liquidfill": "^3.0.6", "echarts-liquidfill": "^3.0.6",
"element-china-area-data": "^4.1.1", "element-china-area-data": "^4.1.1",
"element-ui": "2.15.10", "element-ui": "2.15.10",

View File

@ -1,28 +1,66 @@
export function loadBMap() { // 使用OpenStreetMap和Leaflet替代百度地图
return new Promise(function(resolve, reject) {
if (typeof BMap !== 'undefined') { export function loadMap() {
resolve(BMap)
return true return new Promise(function(resolve, reject) {
} // eslint-disable-next-line no-undef
window.onBMapCallback = function() { if (typeof L !== 'undefined') {
resolve(BMap) // eslint-disable-next-line no-undef
} resolve(L);
// 使用https协议需要添加一下meta标签 return true;
var protocolStr = document.location.protocol; }
if(protocolStr == "https:")
{ // 加载Leaflet CSS
let meta = document.createElement('meta') if (!document.querySelector('link[href*="leaflet.css"]')) {
meta.httpEquiv = 'Content-Security-Policy' const link = document.createElement('link');
meta.content ='upgrade-insecure-requests' link.rel = 'stylesheet';
meta.onerror = reject link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(meta) link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
} link.crossOrigin = '';
// 引入百度地图 document.head.appendChild(link);
let script = document.createElement('script') }
script.type = 'text/javascript'
script.src = // 加载Leaflet JS
'http://api.map.baidu.com/api?v=2.0&ak='+ process.env.VUE_APP_BAI_DU_AK +'&s=1&__ec_v__=20190126&callback=onBMapCallback' const script = document.createElement('script');
script.onerror = reject script.type = 'text/javascript';
document.head.appendChild(script) script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
}) script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=';
script.crossOrigin = '';
script.onload = function() {
// eslint-disable-next-line no-undef
resolve(L);
};
script.onerror = reject;
document.head.appendChild(script);
});
} }
// export function loadBMap() {
// return new Promise(function(resolve, reject) {
// if (typeof BMap !== 'undefined') {
// resolve(BMap)
// return true
// }
// window.onBMapCallback = function() {
// resolve(BMap)
// }
// // 使用https协议需要添加一下meta标签
// var protocolStr = document.location.protocol;
// if(protocolStr == "https:")
// {
// let meta = document.createElement('meta')
// meta.httpEquiv = 'Content-Security-Policy'
// meta.content ='upgrade-insecure-requests'
// meta.onerror = reject
// document.head.appendChild(meta)
// }
// // 引入百度地图
// let script = document.createElement('script')
// script.type = 'text/javascript'
// script.src =
// 'http://api.map.baidu.com/api?v=2.0&ak='+ process.env.VUE_APP_BAI_DU_AK +'&s=1&__ec_v__=20190126&callback=onBMapCallback'
// script.onerror = reject
// document.head.appendChild(script)
// })
// }

View File

@ -15,7 +15,7 @@
<script> <script>
import { import {
loadBMap loadMap
} from '@/utils/map.js' } from '@/utils/map.js'
//安装的是echarts完整包,里面包含百度地图扩展,路径为 echarts/extension/bmap/bmap.js,将其引入 //安装的是echarts完整包,里面包含百度地图扩展,路径为 echarts/extension/bmap/bmap.js,将其引入
//ECharts的百度地图扩展,可以在百度地图上展现点图,线图,热力图等可视化 //ECharts的百度地图扩展,可以在百度地图上展现点图,线图,热力图等可视化
@ -65,7 +65,7 @@ export default {
}, },
/**加载地图*/ /**加载地图*/
loadMap() { loadMap() {
loadBMap().then(() => { loadMap().then(() => {
this.getmap(); this.getmap();
}); });
}, },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,19 +31,25 @@
</div> </div>
<div class="item-content"> <div class="item-content">
<div class="content-value"> <div class="content-value">
<!-- 根据format选择显示HEX或ASCII格式 --> <!-- 自动识别并展示所有可用格式 -->
<div v-if="format === 'Hex' && item.value" class="data-format-item"> <!-- JSON格式优先展示(如果是有效JSON) -->
<span class="format-label">HEX:</span> <div v-if="isValidJson(item.valueAscii || item.value)" class="data-format-item json-item">
<span class="hex-value">{{ item.value }}</span>
</div>
<div v-if="format === 'ASCII' && item.valueAscii" class="data-format-item ascii-item">
<span class="format-label">ASCII:</span>
<pre class="ascii-value">{{ item.valueAscii }}</pre>
</div>
<div v-if="format === 'JSON'" class="data-format-item">
<span class="format-label">JSON:</span> <span class="format-label">JSON:</span>
<pre class="json-value">{{ formatJsonValue(item.value, item.valueAscii) }}</pre> <pre class="json-value">{{ formatJsonValue(item.value, item.valueAscii) }}</pre>
</div> </div>
<!-- 非JSON时展示HEX和ASCII -->
<template v-else>
<!-- HEX格式 -->
<div v-if="item.value" class="data-format-item">
<span class="format-label">HEX:</span>
<span class="hex-value">{{ item.value }}</span>
</div>
<!-- ASCII格式(如果有可读内容) -->
<div v-if="item.valueAscii && hasReadableAscii(item.valueAscii)" class="data-format-item ascii-item">
<span class="format-label">ASCII:</span>
<pre class="ascii-value">{{ item.valueAscii }}</pre>
</div>
</template>
</div> </div>
</div> </div>
<div class="analysis-data" v-if="item.type != 0 && item.analysis"> <div class="analysis-data" v-if="item.type != 0 && item.analysis">
@ -564,6 +570,24 @@ export default {
} }
// 如果都不是JSON,返回提示信息 // 如果都不是JSON,返回提示信息
return '数据不是有效的JSON格式'; return '数据不是有效的JSON格式';
},
/**判断是否为有效的JSON */
isValidJson(value) {
if (!value) return false;
try {
const parsed = JSON.parse(value);
// 确保是对象或数组,而不是简单值
return typeof parsed === 'object' && parsed !== null;
} catch (e) {
return false;
}
},
/**判断ASCII是否包含可读内容 */
hasReadableAscii(asciiValue) {
if (!asciiValue) return false;
// 检查是否有可打印字符(排除纯控制字符)
const printableRegex = /[\x20-\x7E]/;
return printableRegex.test(asciiValue) && asciiValue.trim().length > 0;
}, },
/**打开编辑框 */ /**打开编辑框 */
openEdit() { openEdit() {
@ -990,6 +1014,11 @@ $right-btn-color: #1890ff;
} }
.right { .right {
display: flex;
flex-direction: column;
// 高度 = 头部36px + 内容700px + 底部64px
height: 800px;
.right-head { .right-head {
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
@ -997,19 +1026,28 @@ $right-btn-color: #1890ff;
justify-content: center; justify-content: center;
} }
// 覆盖通用 .content 样式
.content.beautify-scroll-def {
flex: 1;
height: auto;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 0;
}
.quick-item { .quick-item {
width: 80%; width: 80%;
margin: auto; margin: 0 auto 12px auto;
text-align: center; text-align: center;
line-height: 36px; line-height: 36px;
margin-top: 12px;
border: 1px solid $right-btn-color; border: 1px solid $right-btn-color;
color: $right-btn-color; color: $right-btn-color;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
&:last-child { &:first-child {
margin-bottom: 12px; margin-top: 12px;
} }
} }