From 4d9e8c5b00fa3b6d83d99063a8f8919fd2e6dd38 Mon Sep 17 00:00:00 2001 From: bestfeng1020 <31398465+bestfeng1020@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:41:26 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(README):=20=E4=BF=AE=E6=AD=A3=E4=BA=8C?= =?UTF-8?q?=E6=AC=A1=E5=BC=80=E5=8F=91=E6=96=87=E6=A1=A3=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=20(#571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(README): 修正二次开发文档链接 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57cd5987..4ae940c0 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统 | 基础问题答疑 | 问题答疑 | 免费 | 技术交流群支持 [![QQ⑤群554591908](https://img.shields.io/badge/QQ⑤群-554591908-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=jiirLiyFUecy_gsankzVQ-cl6SrZCnv9&&jump_from=webapi) [![QQ④群780133058](https://img.shields.io/badge/QQ④群-780133058-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=Gj47w9kg7TlV5ceD5Bqew_M_O0PIjh_l&jump_from=webapi) [![QQ③群647954464](https://img.shields.io/badge/QQ③群-647954464-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=K5m27CkhDn3B_Owr-g6rfiTBC5DKEY59&jump_from=webapi) [![QQ②群324606263](https://img.shields.io/badge/QQ②群-324606263-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi) [![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi) | | 系统部署 | 系统部署 | 免费 | 文档自助。[源码部署](https://hanta.yuque.com/px7kg1/yfac2l/vvoa3u2ztymtp4oh) [Docker部署](https://hanta.yuque.com/px7kg1/yfac2l/mzq23z4iey5ev1a5) | | 产品使用 | 教学产品各功能使用 | 免费 | 文档自助。[产品文档](https://hanta.yuque.com/px7kg1/yfac2l) | -| 二次开发 | 教学平台源码开发过程、工具使用等;| 免费 | 文档自助。[开发文档](https://hanta.yuque.com/px7kg1/nn1gdr) | +| 二次开发 | 教学平台源码开发过程、工具使用等;| 免费 | 文档自助。[开发文档](https://hanta.yuque.com/px7kg1/dev) | | 系统部署 | 在客户指定的网络和硬件环境中完成社区版服务部署;提供**模拟**设备接入到平台中,并能完成正常设备上线、数据上下行 | 199元 | 线上部署支持 | | 技术支持 | 提供各类部署、功能使用中遇到的问题答疑 | 100元 | 半小时内 线上远程支持| | 设备接入协议开发 | 根据提供的设备型号,编写并提供接入平台协议包的源码。| 3000+元 | 定制化开发 | From 0f78fec18f776c066f9160b12c79d81d8c95cc46 Mon Sep 17 00:00:00 2001 From: fighter-wang <118291973+fighter-wang@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:57:10 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86):?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=E8=A7=A3=E6=9E=90=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=BA=E5=B1=9E=E6=80=A7=E7=89=A9=E6=A8=A1=E5=9E=8B=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20(#569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> --- .../community/io/utils/FileUtils.java | 5 +- .../community/io/utils/UrlCodecUtils.java | 71 +++++++ .../device/web/DeviceInstanceController.java | 39 +++- .../web/excel/DeviceExcelConstants.java | 42 ++++ .../PropertyMetadataExcelImportInfo.java | 201 ++++++++++++++++++ .../excel/PropertyMetadataImportWrapper.java | 61 ++++++ 6 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/UrlCodecUtils.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java diff --git a/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/FileUtils.java b/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/FileUtils.java index 5239de82..c5688453 100644 --- a/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/FileUtils.java +++ b/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/FileUtils.java @@ -1,7 +1,6 @@ package org.jetlinks.community.io.utils; import io.netty.buffer.ByteBufAllocator; -import lombok.SneakyThrows; import org.apache.commons.io.FilenameUtils; import org.jetlinks.core.message.codec.http.HttpUtils; import org.springframework.core.io.Resource; @@ -23,7 +22,9 @@ import java.nio.file.Paths; public class FileUtils { public static String getExtension(String url) { - url = HttpUtils.urlDecode(url); + if (UrlCodecUtils.hasEncode(url)){ + url = HttpUtils.urlDecode(url); + } if (url.contains("?")) { url = url.substring(0, url.lastIndexOf("?")); } diff --git a/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/UrlCodecUtils.java b/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/UrlCodecUtils.java new file mode 100644 index 00000000..96923383 --- /dev/null +++ b/jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/utils/UrlCodecUtils.java @@ -0,0 +1,71 @@ +package org.jetlinks.community.io.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.BitSet; + +/** + * @author bestfeng + */ +public class UrlCodecUtils { + + + static BitSet dontNeedEncoding; + + static { + dontNeedEncoding = new BitSet(128); + int i; + for (i = 'a'; i <= 'z'; i++) { + dontNeedEncoding.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + dontNeedEncoding.set(i); + } + for (i = '0'; i <= '9'; i++) { + dontNeedEncoding.set(i); + } + dontNeedEncoding.set('+'); + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + dontNeedEncoding.set('%'); + } + + /** + * 字符串是否经过了url encode + * + * @param text 字符串 + * @return true表示是 + */ + public static boolean hasEncode(String text) { + if (StringUtils.isBlank(text)) { + return false; + } + for (int i = 0; i < text.length(); i++) { + int c = text.charAt(i); + if (!dontNeedEncoding.get(c)) { + return false; + } + if (c == '%' && (i + 2) < text.length()) { + // 判断是否符合urlEncode规范 + char c1 = text.charAt(++i); + char c2 = text.charAt(++i); + if (!isDigit16Char(c1) || !isDigit16Char(c2)) { + return false; + } + } + } + return true; + } + + /** + * 判断c是否是16进制的字符 + * + * @param c 字符 + * @return true表示是 + */ + private static boolean isDigit16Char(char c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java index 9115888f..da59e08d 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java @@ -37,11 +37,6 @@ import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.device.service.LocalDeviceProductService; import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.community.device.service.data.DeviceProperties; -import org.jetlinks.community.device.web.excel.DeviceExcelImporter; -import org.jetlinks.community.device.web.excel.DeviceExcelInfo; -import org.jetlinks.community.device.web.excel.DeviceWrapper; -import org.jetlinks.community.device.web.excel.PropertyMetadataExcelInfo; -import org.jetlinks.community.device.web.excel.PropertyMetadataWrapper; import org.jetlinks.community.device.web.excel.*; import org.jetlinks.community.device.web.request.AggRequest; import org.jetlinks.community.io.excel.AbstractImporter; @@ -65,11 +60,14 @@ import org.jetlinks.core.message.MessageType; import org.jetlinks.core.message.RepayableDeviceMessage; import org.jetlinks.core.metadata.*; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.data.util.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.StringUtils; @@ -1141,4 +1139,35 @@ public class DeviceInstanceController implements return metricManager .getPropertyMetrics(DeviceThingType.device.getId(), deviceId, property); } + + //仅解析文件为属性物模型 + @PostMapping(value = "/{productId}/property-metadata/file/analyze") + @SaveAction + @Operation(summary = "仅解析文件为属性物模型") + public Mono importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId, + @RequestPart("file") + @Parameter(name = "file", description = "物模型属性文件,支持csv,xlsx文件格式") Mono partMono) { + return partMono + .flatMap(part -> DataBufferUtils + .join(part.content()) + .map(DataBuffer::asInputStream) + .flatMap(inputStream -> metadataManager + .getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device) + .collectList() + .flatMap(configMetadata -> read(inputStream, + FileUtils.getExtension(part + .headers() + .getContentDisposition() + .getFilename()), + new PropertyMetadataImportWrapper(configMetadata)) + .map(PropertyMetadataExcelImportInfo::toMetadata) + .collectList() + .filter(CollectionUtils::isNotEmpty)) + .map(list -> { + SimpleDeviceMetadata metadata = new SimpleDeviceMetadata(); + list.forEach(metadata::addProperty); + return JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata); + })) + ); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java new file mode 100644 index 00000000..31ebae90 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java @@ -0,0 +1,42 @@ +package org.jetlinks.community.device.web.excel; + +/** + * 设备导入导出相关常量 + * @author: wangsheng + */ +public interface DeviceExcelConstants { + /** + * 来源 + */ + String source = "source"; + + /** + * 存储类型 + */ + String storageType = "storageType"; + + /** + * 类型 + */ + String type = "type"; + + /** + * 最大长度 + */ + String maxLength = "macLength"; + + /** + * 单位 + */ + String unit = "unit"; + + /** + * 精度 + */ + String scale = "scale"; + + /** + * 标签 + */ + String tags = "tags"; +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java new file mode 100644 index 00000000..03dcd921 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java @@ -0,0 +1,201 @@ +package org.jetlinks.community.device.web.excel; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.exception.BusinessException; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.metadata.unit.ValueUnit; +import org.jetlinks.core.metadata.unit.ValueUnits; +import org.jetlinks.supports.official.JetLinksDataTypeCodecs; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@Getter +@Setter +@Slf4j +public class PropertyMetadataExcelImportInfo { + + private String property; + + private String name; + + private String valueType; + + private Map expands = new HashMap<>(); + //数据类型 + private String dataType; + //单位 + private String unit; + //精度 + private String scale; + //来源 + private String source; + + private String description; + + private String storageType; + + private long rowNumber; + //读写类型 + private List type; + + /** + * 单位 + */ + private static final List idList = ValueUnits.getAllUnit(); + + /** + * 所有数据类型 + */ + private static final List DATA_TYPES = Lists.newArrayList(ArrayType.ID, BooleanType.ID, + DateTimeType.ID, DoubleType.ID, EnumType.ID, FloatType.ID, IntType.ID, LongType.ID, + ObjectType.ID, StringType.ID, GeoType.ID, FileType.ID, PasswordType.ID); + + private static final List OBJECT_NOT_HAVE = Lists.newArrayList(DateTimeType.ID, FileType.ID, ObjectType.ID, PasswordType.ID); + /** + * 简单模板支持类型 + */ + private static final List SIMPLE = Lists.newArrayList(IntType.ID, FloatType.ID, DoubleType.ID, LongType.ID); + + public void with(String key, Object value) { + FastBeanCopier.copy(Collections.singletonMap(key, value), this); + } + + public void withExpands(String key, Object value) { + FastBeanCopier.copy(Collections.singletonMap(key, value), expands); + } + + public PropertyMetadata toMetadata() { + SimplePropertyMetadata metadata = new SimplePropertyMetadata(); + metadata.setId(property); + metadata.setName(name); + metadata.setValueType(parseDataType()); + metadata.setExpands(parseExpands()); + metadata.setDescription(description); + return metadata; + } + + protected DataType parseDataType() { + JSONObject dataTypeJson = new JSONObject(); + //默认先使用json格式的数据解析物模型,没有json则使用简单模板,只支持int long double float + if (!StringUtils.isEmpty(this.valueType)) { + dataTypeJson = JSON.parseObject(this.valueType); + this.dataType = dataTypeJson.getString(DeviceExcelConstants.type); + } else { + dataTypeJson.put(DeviceExcelConstants.type, this.dataType); + dataTypeJson.put(DeviceExcelConstants.unit, this.unit); + dataTypeJson.put(DeviceExcelConstants.scale, this.scale); + } + DataType dataType = Optional.ofNullable(this.dataType) + .map(DataTypes::lookup) + .map(Supplier::get) + .orElseThrow(() -> new BusinessException("error.unknown_data_type" ,500, this, getDataType())); + JSONObject finalDataTypeJson = dataTypeJson; + JetLinksDataTypeCodecs + .getCodec(dataType.getId()) + .ifPresent(codec -> codec.decode(dataType, finalDataTypeJson)); + return dataType; + } + + protected Map parseExpands() { + // 处理系统默认的扩展信息(中文转换),合并到导入模板的expands中 + expands.put(DeviceExcelConstants.source, PropertySource.getValue(source)); + expands.put(DeviceExcelConstants.storageType, PropertyStorage.getValue(storageType)); + expands.put(DeviceExcelConstants.tags, ""); + expands.put(DeviceExcelConstants.type, type.stream().map(PropertyType::getValue).collect(Collectors.toList())); + return expands; + } + + + public Map toMap() { + setSource(PropertySource.getText(source)); + setStorageType(PropertyStorage.getText(storageType)); + setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, storageType)); + Map map = FastBeanCopier.copy(this, new HashMap<>(8)); + map.put(DeviceExcelConstants.type, type.stream() + .map(PropertyType::getText) + .collect(Collectors.joining(","))); + return map; + } + + @AllArgsConstructor + @Getter + private enum PropertySource implements EnumDict { + device("设备"), + manual("手动"), + rule("规则"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertySource.class, value).map(PropertySource::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertySource.class, text).map(PropertySource::getValue).orElse(""); + } + } + + @AllArgsConstructor + @Getter + private enum PropertyType implements EnumDict { + read("读"), + write("写"), + report("上报"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertyType.class, value).map(PropertyType::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertyType.class, text).map(PropertyType::getValue).orElse(""); + } + } + + @AllArgsConstructor + @Getter + private enum PropertyStorage implements EnumDict { + direct("存储"), + ignore("不存储"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertyStorage.class, value).map(PropertyStorage::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertyStorage.class, text).map(PropertyStorage::getValue).orElse(""); + } + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java new file mode 100644 index 00000000..b242904f --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java @@ -0,0 +1,61 @@ +package org.jetlinks.community.device.web.excel; + +import org.hswebframework.reactor.excel.Cell; +import org.hswebframework.reactor.excel.converter.RowWrapper; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.ConfigPropertyMetadata; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + public class PropertyMetadataImportWrapper extends RowWrapper { + + private final Map propertyMapping = new HashMap<>(); + private final Map expandsMapping = new HashMap<>(); + + public PropertyMetadataImportWrapper(List expands) { + propertyMapping.put("属性ID", "property"); + propertyMapping.put("属性名称", "name"); + propertyMapping.put("数据类型", "dataType"); + propertyMapping.put("单位", "unit"); + propertyMapping.put("精度", "scale"); + propertyMapping.put("数据类型配置", "valueType"); + propertyMapping.put("来源", "source"); + propertyMapping.put("属性说明", "description"); + propertyMapping.put("读写类型", "type"); + propertyMapping.put("存储方式", "storageType"); + for (ConfigMetadata expand : expands) { + for (ConfigPropertyMetadata property : expand.getProperties()) { + expandsMapping.put(expand.getName() + "-" + property.getName(), property.getProperty()); + expandsMapping.put(property.getName(), property.getProperty()); + } + } + } + + @Override + protected PropertyMetadataExcelImportInfo newInstance() { + return new PropertyMetadataExcelImportInfo(); + } + + @Override + protected PropertyMetadataExcelImportInfo wrap(PropertyMetadataExcelImportInfo instance, Cell header, Cell dataCell) { + String headerText = header.valueAsText().orElse("null"); + Object value = dataCell.valueAsText().orElse(""); + if (propertyMapping.containsKey(headerText)) { + instance.with(propertyMapping.get(headerText), propertyTypeToLowerCase(headerText, value)); + } + if (expandsMapping.containsKey(headerText)) { + instance.withExpands(expandsMapping.get(headerText), propertyTypeToLowerCase(headerText, value)); + } + instance.setRowNumber(dataCell.getRowIndex() + 1); + return instance; + } + + private Object propertyTypeToLowerCase(String headerText, Object value) { + if ("类型".equals(headerText)) { + return value.toString().toLowerCase(); + } + return value; + } +}