Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
01d0a033d3
|
|
@ -100,7 +100,7 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
|
|||
| 基础问题答疑 | 问题答疑 | 免费 | 技术交流群支持 [](https://qm.qq.com/cgi-bin/qm/qr?k=jiirLiyFUecy_gsankzVQ-cl6SrZCnv9&&jump_from=webapi) [](https://qm.qq.com/cgi-bin/qm/qr?k=Gj47w9kg7TlV5ceD5Bqew_M_O0PIjh_l&jump_from=webapi) [](https://qm.qq.com/cgi-bin/qm/qr?k=K5m27CkhDn3B_Owr-g6rfiTBC5DKEY59&jump_from=webapi) [](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi) [](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+元 | 定制化开发 |
|
||||
|
|
|
|||
|
|
@ -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("?"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue