Merge remote-tracking branch 'origin/master'

This commit is contained in:
zhouhao 2024-09-12 15:41:18 +08:00
commit 01d0a033d3
7 changed files with 413 additions and 8 deletions

View File

@ -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+元 | 定制化开发 |

View File

@ -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("?"));
}

View File

@ -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');
}
}

View File

@ -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<String> importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId,
@RequestPart("file")
@Parameter(name = "file", description = "物模型属性文件,支持csv,xlsx文件格式") Mono<FilePart> 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);
}))
);
}
}

View File

@ -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";
}

View File

@ -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<String, Object> 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<String> type;
/**
* 单位
*/
private static final List<ValueUnit> idList = ValueUnits.getAllUnit();
/**
* 所有数据类型
*/
private static final List<String> 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<String> OBJECT_NOT_HAVE = Lists.newArrayList(DateTimeType.ID, FileType.ID, ObjectType.ID, PasswordType.ID);
/**
* 简单模板支持类型
*/
private static final List<String> 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<String, Object> 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<String, Object> toMap() {
setSource(PropertySource.getText(source));
setStorageType(PropertyStorage.getText(storageType));
setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, storageType));
Map<String, Object> 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<String> {
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<String> {
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<String> {
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("");
}
}
}

View File

@ -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<PropertyMetadataExcelImportInfo> {
private final Map<String, String> propertyMapping = new HashMap<>();
private final Map<String, String> expandsMapping = new HashMap<>();
public PropertyMetadataImportWrapper(List<ConfigMetadata> 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;
}
}