Merge remote-tracking branch 'yudao/feature/iot' into iot

# Conflicts:
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/tdengine/IotThingModelMessageServiceImpl.java
This commit is contained in:
puhui999
2024-12-23 09:47:59 +08:00
38 changed files with 753 additions and 344 deletions

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.iot.api.device;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceDataService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 设备数据 API 实现类
*/
@Service
@Validated
public class DeviceDataApiImpl implements DeviceDataApi {
@Resource
private IotDeviceDataService deviceDataService;
@Override
public void saveDeviceData(String productKey, String deviceName, String message) {
deviceDataService.saveDeviceData(productKey, deviceName, message);
}
}

View File

@@ -0,0 +1,6 @@
/**
* 占位
*
* TODO 芋艿:后续删除
*/
package cn.iocoder.yudao.module.iot.api;

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
import cn.iocoder.yudao.module.iot.api.Greeting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 打招呼 测试用例
*/
@Component
public class Greetings {
@Autowired
private List<Greeting> greetings;
public Integer printGreetings() {
System.out.printf("找到扩展点的 %d 个扩展 '%s'%n", greetings.size(), Greeting.class.getName());
for (Greeting greeting : greetings) {
System.out.println(">>> " + greeting.getGreeting());
}
return greetings.size();
}
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
import jakarta.annotation.Resource;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -25,12 +24,8 @@ import java.util.stream.Collectors;
@RequestMapping("/iot/plugins")
public class PluginController {
@Resource
private ApplicationContext applicationContext;
@Resource
private SpringPluginManager springPluginManager;
@Resource
private Greetings greetings;
@Value("${pf4j.pluginsDir}")
private String pluginsDir;
@@ -73,10 +68,8 @@ public class PluginController {
return ResponseEntity.ok("插件上传并加载成功");
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传插件时发生错误: " + e.getMessage());
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("加载插件时发生错误: " + e.getMessage());
}
}
@@ -120,15 +113,4 @@ public class PluginController {
return ResponseEntity.ok(plugins);
}
/**
* 打印问候语
*
* @return 问候语数量
*/
@PermitAll
@GetMapping("/printGreetings")
public ResponseEntity<Integer> printGreetings() {
Integer count = greetings.printGreetings();
return ResponseEntity.ok(count);
}
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
import cn.iocoder.yudao.module.iot.api.Greeting;
import org.pf4j.Extension;
import org.springframework.stereotype.Component;
/**
* 打招呼 测试用例
*/
@Extension
@Component
public class WhazzupGreeting implements Greeting {
@Override
public String getGreeting() {
return "Whazzup";
}
}

View File

@@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;

View File

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
@Schema(description = "管理后台 - IoT 插件实例新增/修改 Request VO")

View File

@@ -10,7 +10,7 @@ import org.apache.ibatis.annotations.Mapper;
*/
@Mapper
@DS("tdengine")
public interface TdThinkModelMessageMapper {
public interface TdThingModelMessageMapper {
/**
* 创建物模型消息日志超级表超级表

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.framework.plugin;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
@Slf4j
@Configuration
public class ServiceRegistryConfiguration {
@Resource
private DeviceDataApi deviceDataApi;
@PostConstruct
public void init() {
// 将主程序中的 DeviceDataApi 实例注册到 ServiceRegistry
ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi);
log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]");
}
/**
* 定义一个标记用的 Bean用于表示 ServiceRegistry 已初始化完成
*/
@Bean("serviceRegistryInitializedMarker") // TODO @haohao1这个名字可以搞个 public static final 常量2是不是 conditionBefore 啥
public Object serviceRegistryInitializedMarker() {
// 返回任意对象即可,这里返回 null 都可以,但最好返回个实际对象
return new Object();
}
}

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.framework.plugin;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.Greetings;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -10,14 +9,9 @@ import org.springframework.context.annotation.DependsOn;
public class SpringConfiguration {
@Bean
@DependsOn("serviceRegistryInitializedMarker")
public SpringPluginManager pluginManager() {
return new SpringPluginManager();
}
@Bean
@DependsOn("pluginManager")
public Greetings greetings() {
return new Greetings();
}
}

View File

@@ -0,0 +1,10 @@
package cn.iocoder.yudao.module.iot.mq.consumer.simulatesend;
/**
* TODO @alwayssuper记得实现还有类注释哈
*
* @author alwayssuper
* @since 2024/12/20 8:04
*/
public class SimulateSendConsumer {
}

View File

@@ -20,7 +20,8 @@ public interface IotDeviceDataService {
*
* @param productKey 产品 key
* @param deviceName 设备名称
* @param message 消息
* @param message 消息
* <p>参见 <a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services?spm=a2c4g.11186623.0.0.3a3335aeUdzkz2#concept-mvc-4tw-y2b">JSON 格式</a>
*/
void saveDeviceData(String productKey, String deviceName, String message);

View File

@@ -22,9 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
@@ -220,24 +218,24 @@ public class PluginInfoServiceImpl implements PluginInfoService {
pluginInfoMapper.updateById(pluginInfoDo);
}
@PostConstruct
public void init() {
Executors.newSingleThreadScheduledExecutor().schedule(this::startPlugins, 3, TimeUnit.SECONDS);
}
@SneakyThrows
private void startPlugins() {
for (PluginInfoDO pluginInfoDO : pluginInfoMapper.selectList()) {
if (!IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
continue;
}
log.info("start plugin:{}", pluginInfoDO.getPluginId());
try {
pluginManager.startPlugin(pluginInfoDO.getPluginId());
} catch (Exception e) {
log.error("start plugin error", e);
}
}
}
// @PostConstruct
// public void init() {
// Executors.newSingleThreadScheduledExecutor().schedule(this::startPlugins, 3, TimeUnit.SECONDS);
// }
//
// @SneakyThrows
// private void startPlugins() {
// for (PluginInfoDO pluginInfoDO : pluginInfoMapper.selectList()) {
// if (!IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
// continue;
// }
// log.info("start plugin:{}", pluginInfoDO.getPluginId());
// try {
// pluginManager.startPlugin(pluginInfoDO.getPluginId());
// } catch (Exception e) {
// log.error("start plugin error", e);
// }
// }
// }
}

View File

@@ -13,6 +13,7 @@ import org.springframework.validation.annotation.Validated;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PLUGIN_INSTANCE_NOT_EXISTS;
// TODO @haohao可以搞个 plugin 包,然后把 plugininfo、plugininstance
/**
* IoT 插件实例 Service 实现类
*

View File

@@ -7,19 +7,21 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.FieldParser;
import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.TdFieldDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.TdTableDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO;
import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDDLMapper;
import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper;
import cn.iocoder.yudao.module.iot.dal.tdengine.TdThinkModelMessageMapper;
import cn.iocoder.yudao.module.iot.enums.IotConstants;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotProductThingModelService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.util.IotTdDatabaseUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -64,11 +66,6 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
@Resource
private DeviceDataRedisDAO deviceDataRedisDAO;
@Resource
private IotTdDatabaseUtils iotTdDatabaseUtils;
@Resource
private TdThinkModelMessageMapper tdThinkModelMessageMapper;
// TODO @haohao这个方法可以考虑加下 1. 2. 3. 更有层次感
@Override
@@ -81,7 +78,7 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
iotDeviceService.updateDeviceStatus(new IotDeviceStatusUpdateReqVO()
.setId(device.getId()).setStatus(IotDeviceStatusEnum.ONLINE.getStatus()));
// 1.2 创建物模型日志设备表
createThinkModelMessageDeviceTable(device.getProductKey(), device.getDeviceName(), device.getDeviceKey());
createThingModelMessageDeviceTable(device.getProductKey(), device.getDeviceName(), device.getDeviceKey());
}
// 2. 获取设备属性并进行物模型校验,过滤非物模型属性
@@ -118,13 +115,23 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
// 2. 获取超级表的名称和数据库名称
// TODO @alwayssuper最好 databaseName、superTableName 的处理,放到 tdThinkModelMessageMapper 里。可以考虑,弄个 default 方法
String databaseName = iotTdDatabaseUtils.getDatabaseName();
String databaseName = IotTdDatabaseUtils.getDatabaseName(url);
String superTableName = IotTdDatabaseUtils.getThingModelMessageSuperTableName(product.getProductKey());
// 解析物模型,获取字段列表
List<TdFieldDO> schemaFields = List.of(
TdFieldDO.builder().fieldName("time").dataType("TIMESTAMP").build(),
TdFieldDO.builder().fieldName("id").dataType("NCHAR").dataLength(64).build(),
TdFieldDO.builder().fieldName("sys").dataType("NCHAR").dataLength(2048).build(),
TdFieldDO.builder().fieldName("method").dataType("NCHAR").dataLength(256).build(),
TdFieldDO.builder().fieldName("params").dataType("NCHAR").dataLength(2048).build()
);
// 设置超级表的标签
List<TdFieldDO> tagsFields = List.of(
TdFieldDO.builder().fieldName("device_key").dataType("NCHAR").dataLength(64).build()
);
// 3. 创建超级表
tdThinkModelMessageMapper.createSuperTable(ThingModelMessageDO.builder().build()
.setDataBaseName(databaseName)
.setSuperTableName(superTableName));
tdEngineDDLMapper.createSuperTable(new TdTableDO(databaseName, superTableName, schemaFields, tagsFields));
}
private List<IotProductThingModelDO> getValidFunctionList(String productKey) {
@@ -227,21 +234,22 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param deviceKey 设备 Key
*
*/
private void createThinkModelMessageDeviceTable(String productKey, String deviceName, String deviceKey) {
private void createThingModelMessageDeviceTable(String productKey, String deviceName, String deviceKey){
// 1. 获取超级表的名称、数据库名称、设备日志表名称
String databaseName = iotTdDatabaseUtils.getDatabaseName();
String databaseName = IotTdDatabaseUtils.getDatabaseName(url);
String superTableName = IotTdDatabaseUtils.getThingModelMessageSuperTableName(productKey);
// TODO @alwayssuper最好 databaseName、superTableName、thinkModelMessageDeviceTableName 的处理,放到 tdThinkModelMessageMapper 里。可以考虑,弄个 default 方法
String thinkModelMessageDeviceTableName = IotTdDatabaseUtils.getThinkModelMessageDeviceTableName(productKey, deviceName);
String thinkModelMessageDeviceTableName = IotTdDatabaseUtils.getThingModelMessageDeviceTableName(productKey, deviceName);
// 2. 创建物模型日志设备数据表
tdThinkModelMessageMapper.createTableWithTag(ThingModelMessageDO.builder().build()
.setDataBaseName(databaseName)
.setSuperTableName(superTableName)
.setTableName(thinkModelMessageDeviceTableName)
.setDeviceKey(deviceKey));
// tdThingModelMessageMapper.createTableWithTag(ThingModelMessageDO.builder().build()
// .setDataBaseName(databaseName)
// .setSuperTableName(superTableName)
// .setTableName(thinkModelMessageDeviceTableName)
// .setDeviceKey(deviceKey));
}
/**

View File

@@ -1,8 +1,9 @@
package cn.iocoder.yudao.module.iot.util;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.enums.IotConstants;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
// TODO @芋艿:可能要思索下,有没更好的处理方式
// TODO @芋艿:怎么改成无状态
@@ -11,19 +12,14 @@ import org.springframework.stereotype.Component;
*
* @author AlwaysSuper
*/
@Component
public class IotTdDatabaseUtils {
@Value("${spring.datasource.dynamic.datasource.tdengine.url}")
private String url;
/**
* 获取数据库名称
*/
public String getDatabaseName() {
public static String getDatabaseName(String url) {
// TODO @alwayssuper:StrUtil.subAfter("/")
int index = url.lastIndexOf("/");
return index != -1 ? url.substring(index + 1) : url;
return StrUtil.subAfter(url, "/", true);
}
/**
@@ -34,12 +30,17 @@ public class IotTdDatabaseUtils {
* @return 产品超级表表名
*/
public static String getProductSuperTableName(Integer deviceType, String productKey) {
// TODO @alwayssuper枚举字段不要 1、2、3不符合预期抛出异常
return switch (deviceType) {
case 1 -> String.format(IotConstants.GATEWAY_SUB_STABLE_NAME_FORMAT, productKey).toLowerCase();
case 2 -> String.format(IotConstants.GATEWAY_STABLE_NAME_FORMAT, productKey).toLowerCase();
default -> String.format(IotConstants.DEVICE_STABLE_NAME_FORMAT, productKey).toLowerCase();
};
Assert.notNull(deviceType, "deviceType 不能为空");
if (IotProductDeviceTypeEnum.GATEWAY_SUB.getType().equals(deviceType)) {
return String.format(IotConstants.GATEWAY_SUB_STABLE_NAME_FORMAT, productKey).toLowerCase();
}
if (IotProductDeviceTypeEnum.GATEWAY.getType().equals(deviceType)) {
return String.format(IotConstants.GATEWAY_STABLE_NAME_FORMAT, productKey).toLowerCase();
}
if (IotProductDeviceTypeEnum.DIRECT.getType().equals(deviceType)){
return String.format(IotConstants.DEVICE_STABLE_NAME_FORMAT, productKey).toLowerCase();
}
throw new IllegalArgumentException("deviceType 不正确");
}
/**
@@ -50,8 +51,7 @@ public class IotTdDatabaseUtils {
*
*/
public static String getThingModelMessageSuperTableName(String productKey) {
// TODO @alwayssuper是不是应该 + 拼接就好,不用 format
return String.format("thing_model_message_", productKey).toLowerCase();
return "thing_model_message_" + productKey.toLowerCase();
}
/**
@@ -61,8 +61,9 @@ public class IotTdDatabaseUtils {
* @param deviceName 设备名称
* @return 物模型日志设备表名
*/
public static String getThinkModelMessageDeviceTableName(String productKey, String deviceName) {
return String.format(IotConstants.THING_MODEL_MESSAGE_TABLE_NAME_FORMAT, productKey.toLowerCase(), deviceName.toLowerCase());
public static String getThingModelMessageDeviceTableName(String productKey, String deviceName) {
return String.format(IotConstants.THING_MODEL_MESSAGE_TABLE_NAME_FORMAT,
productKey.toLowerCase(), deviceName.toLowerCase());
}
}

View File

@@ -2,10 +2,9 @@
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.TdThinkModelMessageMapper">
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.TdThingModelMessageMapper">
<!-- 创建物模型消息日志超级表 -->
<!-- TODO @芋艿:捉摸下字段,特别是 sys、ts 这种缩写 -->
<update id="createSuperTable">
CREATE STABLE ${dataBaseName}.${superTableName}(
ts TIMESTAMP,
@@ -14,7 +13,7 @@
method VARCHAR(255),
params VARCHAR(2048)
)TAGS (
deviceKey VARCHAR(255)
device_key VARCHAR(255)
)
</update>
@@ -28,7 +27,7 @@
method ,
params
)TAGS(
#{deviceKey}
#{device_key}
)
</update>
</mapper>