Merge branch 'feature/iot' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into origin/feature/iot

This commit is contained in:
alwayssuper 2024-12-31 14:17:50 +08:00
commit 1ce9420a8d
100 changed files with 1766 additions and 1905 deletions

View File

@ -0,0 +1,2 @@
http-plugin
http-plugin@0.0.1

View File

@ -66,7 +66,9 @@ public class CrmReceivableDO extends BaseDO {
*/
private LocalDateTime returnTime;
/**
* 回款方式,关联枚举{@link CrmReceivableReturnTypeEnum}
* 回款方式
*
* 枚举 {@link CrmReceivableReturnTypeEnum}
*/
private Integer returnType;
/**

View File

@ -22,6 +22,7 @@
<artifactId>yudao-common</artifactId>
</dependency>
<!-- PF4J -->
<!-- TODO 芋艿:这个依赖,要不要放在 api 包 -->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>

View File

@ -7,6 +7,7 @@ import java.util.Map;
* 服务注册表 - 插架模块使用无法使用 Spring 注入
*/
public class ServiceRegistry {
private static final Map<Class<?>, Object> services = new HashMap<>();
/**
@ -31,4 +32,5 @@ public class ServiceRegistry {
public static <T> T getService(Class<T> serviceClass) {
return (T) services.get(serviceClass);
}
}

View File

@ -14,7 +14,6 @@ public interface ErrorCodeConstants {
ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在");
ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除");
ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型");
ErrorCode PRODUCT_DEVICE_NOT_EXISTS = new ErrorCode(1_050_001_004, "产品设备类型不存在");
// ========== 产品物模型 1-050-002-000 ============
ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在");

View File

@ -34,12 +34,10 @@ public enum IotPluginDeployTypeEnum implements IntArrayValuable {
}
public static IotPluginDeployTypeEnum fromDeployType(Integer deployType) {
for (IotPluginDeployTypeEnum value : values()) {
if (value.getDeployType().equals(deployType)) {
return value;
}
}
return null;
return Arrays.stream(values())
.filter(value -> value.getDeployType().equals(deployType))
.findFirst()
.orElse(null);
}
public static boolean isValidDeployType(Integer deployType) {

View File

@ -34,12 +34,10 @@ public enum IotPluginStatusEnum implements IntArrayValuable {
}
public static IotPluginStatusEnum fromState(Integer state) {
for (IotPluginStatusEnum value : values()) {
if (value.getStatus().equals(state)) {
return value;
}
}
return null;
return Arrays.stream(values())
.filter(value -> value.getStatus().equals(state))
.findFirst()
.orElse(null);
}
@Override

View File

@ -35,14 +35,11 @@ public enum IotPluginTypeEnum implements IntArrayValuable {
return ARRAYS;
}
// TODO @haohao可以使用 hutool 简化
public static IotPluginTypeEnum fromType(Integer type) {
for (IotPluginTypeEnum value : values()) {
if (value.getType().equals(type)) {
return value;
}
}
return null;
return Arrays.stream(values())
.filter(value -> value.getType().equals(type))
.findFirst()
.orElse(null);
}
public static boolean isValidType(Integer type) {

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.enums.thingmodel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IoT 数据定义的数据类型枚举类
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum IotDataSpecsDataTypeEnum {
INT("int"),
FLOAT("float"),
DOUBLE("double"),
ENUM("enum"),
BOOL("bool"),
TEXT("text"),
DATE("date"),
STRUCT("struct"),
ARRAY("array");
private final String dataType;
}

View File

@ -4,13 +4,13 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IOT 访问方式枚举类
* IOT 产品物模型属性读取类型枚举
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotProductThingModelAccessModeEnum {
public enum IotThingModelAccessModeEnum {
READ_ONLY("r"),
READ_WRITE("rw");

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.iot.enums.thingmodel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IOT 产品物模型参数是输入参数还是输出参数枚举
*
* @author HUIHUI
*/
@AllArgsConstructor
@Getter
public enum IotThingModelParamDirectionEnum {
INPUT("input"), // 输入参数
OUTPUT("output"); // 输出参数
private final String direction;
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.iot.enums.thingmodel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IOT 产品物模型服务调用方式枚举
*
* @author HUIHUI
*/
@AllArgsConstructor
@Getter
public enum IotThingModelServiceCallTypeEnum {
ASYNC("async"), // 异步调用
SYNC("sync"); // 同步调用
private final String type;
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.iot.enums.thingmodel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IOT 产品物模型事件类型枚举
*
* @author HUIHUI
*/
@AllArgsConstructor
@Getter
public enum IotThingModelServiceEventTypeEnum {
INFO("info"), // 信息
ALERT("alert"), // 告警
ERROR("error"); // 故障
private final String type;
}

View File

@ -13,13 +13,13 @@ import java.util.Arrays;
*/
@AllArgsConstructor
@Getter
public enum IotProductThingModelTypeEnum implements IntArrayValuable {
public enum IotThingModelTypeEnum implements IntArrayValuable {
PROPERTY(1, "属性"),
SERVICE(2, "服务"),
EVENT(3, "事件");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotProductThingModelTypeEnum::getType).toArray();
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotThingModelTypeEnum::getType).toArray();
/**
* 类型
@ -30,8 +30,8 @@ public enum IotProductThingModelTypeEnum implements IntArrayValuable {
*/
private final String description;
public static IotProductThingModelTypeEnum valueOfType(Integer type) {
for (IotProductThingModelTypeEnum value : values()) {
public static IotThingModelTypeEnum valueOfType(Integer type) {
for (IotThingModelTypeEnum value : values()) {
if (value.getType().equals(type)) {
return value;
}

View File

@ -25,13 +25,6 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-plugin-api</artifactId>
<version>0.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
@ -81,6 +74,7 @@
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<!-- TODO @芋艿:可以放到 bom 里配置 -->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.iot.api.device;
import cn.iocoder.yudao.module.iot.api.DeviceDataApi;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceDataService;
import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@ -15,11 +14,11 @@ import javax.annotation.Resource;
public class DeviceDataApiImpl implements DeviceDataApi {
@Resource
private IotDeviceDataService iotDeviceDataService;
private IotDevicePropertyDataService deviceDataService;
@Override
public void saveDeviceData(String productKey, String deviceName, String message) {
iotDeviceDataService.saveDeviceData(productKey, deviceName, message);
deviceDataService.saveDeviceData(productKey, deviceName, message);
}
}

View File

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

View File

@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDevi
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotTimeDataRespVO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceDataService;
import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@ -29,7 +29,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
public class IotDeviceDataController {
@Resource
private IotDeviceDataService deviceDataService;
private IotDevicePropertyDataService deviceDataService;
// TODO @浩浩这里的 /latest-list包括方法名
@GetMapping("/latest")

View File

@ -1,13 +1,14 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoImportReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import cn.iocoder.yudao.module.iot.service.plugininfo.PluginInfoService;
import cn.iocoder.yudao.module.iot.service.plugin.PluginInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -16,11 +17,8 @@ import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
@Tag(name = "管理后台 - IoT 插件信息")
@RestController
@ -72,16 +70,11 @@ public class PluginInfoController {
return success(BeanUtils.toBean(pageResult, PluginInfoRespVO.class));
}
@RequestMapping(value = "/update-jar",
method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
@Operation(summary = "上传Jar包")
public CommonResult<Boolean> uploadJar(
@RequestParam("id") Long id,
@RequestParam("jar") MultipartFile file) throws Exception {
if (file.isEmpty()) {
throw exception(FILE_IS_EMPTY);
}
pluginInfoService.uploadJar(id, file);
@PostMapping("/upload-file")
@Operation(summary = "上传插件文件")
@PreAuthorize("@ss.hasPermission('iot:plugin-info:update')")
public CommonResult<Boolean> uploadFile(@Valid PluginInfoImportReqVO reqVO) {
pluginInfoService.uploadFile(reqVO.getId(), reqVO.getFile());
return success(true);
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Schema(description = "管理后台 - IoT 插件上传 Request VO")
@Data
public class PluginInfoImportReqVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
private Long id;
@Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "插件文件不能为空")
private MultipartFile file;
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IoT 插件信息分页 Request VO")
@Data
public class PluginInfoPageReqVO extends PageParam {
@Schema(description = "插件名称", example = "http")
private String name;
@Schema(description = "状态")
private Integer status;
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
@ -16,9 +16,9 @@ public class PluginInfoRespVO {
@ExcelProperty("主键 ID")
private Long id;
@Schema(description = "插件包 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
@ExcelProperty("插件包 ID")
private String pluginId;
@Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
@ExcelProperty("插件包标识符")
private String pluginKey;
@Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
@ExcelProperty("插件名称")
@ -34,7 +34,7 @@ public class PluginInfoRespVO {
@Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("插件包文件名")
private String file;
private String fileName;
@Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("插件版本")

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@ -10,8 +10,8 @@ public class PluginInfoSaveReqVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
private Long id;
@Schema(description = "插件包id", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
private String pluginId;
@Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
private String pluginKey;
@Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
private String name;
@ -23,7 +23,7 @@ public class PluginInfoSaveReqVO {
private Integer deployType;
@Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED)
private String file;
private String fileName;
@Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED)
private String version;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo;
package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

View File

@ -1,116 +0,0 @@
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.PermitAll;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Collectors;
/**
* 插件 Controller 测试用例
*/
@RestController
@RequestMapping("/iot/plugins")
public class PluginController {
@Resource
private SpringPluginManager springPluginManager;
@Value("${pf4j.pluginsDir}")
private String pluginsDir;
/**
* 上传插件 JAR 文件并加载插件
*
* @param file 上传的 JAR 文件
* @return 上传结果
*/
@PermitAll
@PostMapping("/upload")
public ResponseEntity<String> uploadPlugin(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("上传的文件为空");
}
// 确保插件目录存在
Path pluginsPath = Paths.get(pluginsDir);
try {
if (!Files.exists(pluginsPath)) {
Files.createDirectories(pluginsPath);
}
// 保存上传的 JAR 文件到插件目录
String filename = file.getOriginalFilename();
if (filename == null || (!filename.endsWith(".jar") && !filename.endsWith(".zip"))) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("上传的文件不是 JAR 或 ZIP 文件");
}
Path jarPath = pluginsPath.resolve(filename);
Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING);
// 加载插件
String pluginId = springPluginManager.loadPlugin(jarPath.toAbsolutePath());
// 启动插件
springPluginManager.startPlugin(pluginId);
return ResponseEntity.ok("插件上传并加载成功");
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传插件时发生错误: " + e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("加载插件时发生错误: " + e.getMessage());
}
}
/**
* 卸载指定插件
*
* @param pluginId 插件 ID
* @return 卸载结果
*/
@PermitAll
@DeleteMapping("/unload/{pluginId}")
public ResponseEntity<String> unloadPlugin(@PathVariable String pluginId) {
if (springPluginManager.getPlugins().stream().noneMatch(plugin -> plugin.getDescriptor().getPluginId().equals(pluginId))) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("插件未加载: " + pluginId);
}
springPluginManager.stopPlugin(pluginId);
springPluginManager.unloadPlugin(pluginId);
// 删除插件 JAR 文件可选
// PluginWrapper plugin = pluginManager.getPlugin(pluginId);
// PluginDescriptor descriptor = plugin.getDescriptor();
// Path jarPath = Paths.get(pluginsDir).resolve(descriptor.getPluginId() + ".jar");
// Files.deleteIfExists(jarPath);
return ResponseEntity.ok("插件卸载成功: " + pluginId);
}
/**
* 列出所有已加载的插件
*
* @return 插件列表
*/
@PermitAll
@GetMapping("/list")
public ResponseEntity<List<String>> listPlugins() {
List<String> plugins = springPluginManager.getPlugins().stream()
.map(plugin -> plugin.getDescriptor().getPluginId())
.collect(Collectors.toList());
return ResponseEntity.ok(plugins);
}
}

View File

@ -1,59 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
// TODO @haohao只查询必要字段哈
@Schema(description = "管理后台 - IoT 插件信息分页 Request VO")
@Data
public class PluginInfoPageReqVO extends PageParam {
@Schema(description = "插件包 ID ", example = "24627")
private String pluginId;
@Schema(description = "插件名称", example = "赵六")
private String name;
@Schema(description = "描述", example = "你猜")
private String description;
@Schema(description = "部署方式", example = "2")
private Integer deployType;
@Schema(description = "插件包文件名")
private String file;
@Schema(description = "插件版本")
private String version;
@Schema(description = "插件类型", example = "2")
@InEnum(IotPluginTypeEnum.class)
private Integer type;
@Schema(description = "设备插件协议类型")
private String protocol;
@Schema(description = "状态")
private Integer status;
@Schema(description = "插件配置项描述信息")
private String configSchema;
@Schema(description = "插件配置信息")
private String config;
@Schema(description = "插件脚本")
private String script;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -1,94 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.plugininstance;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
import cn.iocoder.yudao.module.iot.service.plugininstance.PluginInstanceService;
@Tag(name = "管理后台 - IoT 插件实例")
@RestController
@RequestMapping("/iot/plugin-instance")
@Validated
public class PluginInstanceController {
@Resource
private PluginInstanceService pluginInstanceService;
@PostMapping("/create")
@Operation(summary = "创建IoT 插件实例")
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:create')")
public CommonResult<Long> createPluginInstance(@Valid @RequestBody PluginInstanceSaveReqVO createReqVO) {
return success(pluginInstanceService.createPluginInstance(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新IoT 插件实例")
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:update')")
public CommonResult<Boolean> updatePluginInstance(@Valid @RequestBody PluginInstanceSaveReqVO updateReqVO) {
pluginInstanceService.updatePluginInstance(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除IoT 插件实例")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:delete')")
public CommonResult<Boolean> deletePluginInstance(@RequestParam("id") Long id) {
pluginInstanceService.deletePluginInstance(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得IoT 插件实例")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:query')")
public CommonResult<PluginInstanceRespVO> getPluginInstance(@RequestParam("id") Long id) {
PluginInstanceDO pluginInstance = pluginInstanceService.getPluginInstance(id);
return success(BeanUtils.toBean(pluginInstance, PluginInstanceRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得IoT 插件实例分页")
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:query')")
public CommonResult<PageResult<PluginInstanceRespVO>> getPluginInstancePage(@Valid PluginInstancePageReqVO pageReqVO) {
PageResult<PluginInstanceDO> pageResult = pluginInstanceService.getPluginInstancePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, PluginInstanceRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出IoT 插件实例 Excel")
@PreAuthorize("@ss.hasPermission('iot:plugin-instance:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportPluginInstanceExcel(@Valid PluginInstancePageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<PluginInstanceDO> list = pluginInstanceService.getPluginInstancePage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "IoT 插件实例.xls", "数据", PluginInstanceRespVO.class,
BeanUtils.toBean(list, PluginInstanceRespVO.class));
}
}

View File

@ -1,84 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.convert.thingmodel.IotProductThingModelConvert;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotProductThingModelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IoT 产品物模型")
@RestController
@RequestMapping("/iot/product-thing-model")
@Validated
public class IotProductThingModelController {
@Resource
private IotProductThingModelService thingModelService;
@PostMapping("/create")
@Operation(summary = "创建产品物模型")
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:create')")
public CommonResult<Long> createProductThingModel(@Valid @RequestBody IotProductThingModelSaveReqVO createReqVO) {
return success(thingModelService.createProductThingModel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新产品物模型")
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:update')")
public CommonResult<Boolean> updateProductThingModel(@Valid @RequestBody IotProductThingModelSaveReqVO updateReqVO) {
thingModelService.updateProductThingModel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除产品物模型")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:delete')")
public CommonResult<Boolean> deleteProductThingModel(@RequestParam("id") Long id) {
thingModelService.deleteProductThingModel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得产品物模型")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:query')")
public CommonResult<IotProductThingModelRespVO> getProductThingModel(@RequestParam("id") Long id) {
IotProductThingModelDO thingModel = thingModelService.getProductThingModel(id);
return success(IotProductThingModelConvert.INSTANCE.convert(thingModel));
}
@GetMapping("/list-by-product-id")
@Operation(summary = "获得产品物模型")
@Parameter(name = "productId", description = "产品ID", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:query')")
public CommonResult<List<IotProductThingModelRespVO>> getProductThingModelListByProductId(@RequestParam("productId") Long productId) {
List<IotProductThingModelDO> list = thingModelService.getProductThingModelListByProductId(productId);
return success(IotProductThingModelConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@Operation(summary = "获得产品物模型分页")
@PreAuthorize("@ss.hasPermission('iot:product-thing-model:query')")
public CommonResult<PageResult<IotProductThingModelRespVO>> getProductThingModelPage(@Valid IotProductThingModelPageReqVO pageReqVO) {
PageResult<IotProductThingModelDO> pageResult = thingModelService.getProductThingModelPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, IotProductThingModelRespVO.class));
}
}

View File

@ -25,8 +25,7 @@ Authorization: Bearer {{token}}
"defaultValue": "30",
"unit": "%",
"unitName": "百分比"
},
"description": "当前温度值"
}
}
}
@ -46,7 +45,6 @@ Authorization: Bearer {{token}}
"property": {
"identifier": "switch",
"name": "开关",
"description": "温度计开关",
"accessMode": "rw",
"required": true,
"dataType": "bool",
@ -81,7 +79,6 @@ Authorization: Bearer {{token}}
"property": {
"identifier": "argb",
"name": "温度计 argb 颜色",
"description": "温度计 argb 颜色",
"accessMode": "rw",
"required": true,
"dataType": "array",
@ -93,7 +90,6 @@ Authorization: Bearer {{token}}
{
"identifier": "switch",
"name": "开关",
"description": "温度计开关",
"accessMode": "rw",
"required": true,
"dataType": "struct",
@ -126,8 +122,7 @@ Authorization: Bearer {{token}}
"defaultValue": "30",
"unit": "%",
"unitName": "百分比"
},
"description": "当前温度值"
}
}
]
}
@ -151,7 +146,6 @@ Authorization: Bearer {{token}}
"property": {
"identifier": "switch",
"name": "开关",
"description": "温度计开关",
"accessMode": "r",
"required": true,
"dataType": "bool",
@ -176,7 +170,7 @@ tenant-id: {{adminTenentId}}
Authorization: Bearer {{token}}
### 请求 /iot/product-thing-model/get 接口 => 成功
GET {{baseUrl}}/iot/product-thing-model/get?id=40
GET {{baseUrl}}/iot/product-thing-model/get?id=67
tenant-id: {{adminTenentId}}
Authorization: Bearer {{token}}

View File

@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IoT 产品物模型")
@RestController
@RequestMapping("/iot/thing-model")
@Validated
public class IotThingModelController {
@Resource
private IotThingModelService thingModelService;
@PostMapping("/create")
@Operation(summary = "创建产品物模型")
@PreAuthorize("@ss.hasPermission('iot:thing-model:create')")
public CommonResult<Long> createThingModel(@Valid @RequestBody IotThingModelSaveReqVO createReqVO) {
return success(thingModelService.createThingModel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新产品物模型")
@PreAuthorize("@ss.hasPermission('iot:thing-model:update')")
public CommonResult<Boolean> updateThingModel(@Valid @RequestBody IotThingModelSaveReqVO updateReqVO) {
thingModelService.updateThingModel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除产品物模型")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:thing-model:delete')")
public CommonResult<Boolean> deleteThingModel(@RequestParam("id") Long id) {
thingModelService.deleteThingModel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得产品物模型")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:thing-model:query')")
public CommonResult<IotThingModelRespVO> getThingModel(@RequestParam("id") Long id) {
IotThingModelDO thingModel = thingModelService.getThingModel(id);
return success(IotThingModelConvert.INSTANCE.convert(thingModel));
}
@GetMapping("/list-by-product-id")
@Operation(summary = "获得产品物模型")
@Parameter(name = "productId", description = "产品ID", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:thing-model:query')")
public CommonResult<List<IotThingModelRespVO>> getThingModelListByProductId(@RequestParam("productId") Long productId) {
List<IotThingModelDO> list = thingModelService.getThingModelListByProductId(productId);
return success(IotThingModelConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@Operation(summary = "获得产品物模型分页")
@PreAuthorize("@ss.hasPermission('iot:thing-model:query')")
public CommonResult<PageResult<IotThingModelRespVO>> getThingModelPage(@Valid IotThingModelPageReqVO pageReqVO) {
PageResult<IotThingModelDO> pageResult = thingModelService.getProductThingModelPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, IotThingModelRespVO.class));
}
}

View File

@ -1,9 +1,15 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelArgument;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceEventTypeEnum;
import lombok.Data;
import java.util.List;
/**
* 物模型中的事件
*
* @author HUIHUI
*/
@Data
public class ThingModelEvent {
@ -16,17 +22,24 @@ public class ThingModelEvent {
*/
private String name;
/**
* 事件描述
* 是否是标准品类的必选事件
*/
private String description;
private Boolean required;
/**
* 事件类型
*
* "info""alert""error"
* 枚举 {@link IotThingModelServiceEventTypeEnum}
*/
private String type;
private List<ThingModelArgument> outputData;
/**
* 事件的输出参数
*
* 输出参数定义事件调用后返回的结果或反馈信息用于确认操作结果或提供额外的信息
*/
private List<ThingModelParam> outputParams;
/**
* 标识设备需要执行的具体操作
*/
private String method;
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelParamDirectionEnum;
import lombok.Data;
import java.util.List;
/**
* IOT 产品物模型中的参数
*
* @author HUIHUI
*/
@Data
public class ThingModelParam {
/**
* 参数标识符
*/
private String identifier;
/**
* 参数名称
*/
private String name;
/**
* 用于区分输入或输出参数
*
* 枚举 {@link IotThingModelParamDirectionEnum}
*/
private String direction;
/**
* 参数的序号 0 开始排序且不能重复
*
* TODO 考虑要不要序号感觉是要的, 先留一手看看
*/
private Integer paraOrder;
/**
* 参数值的数据类型 dataSpecs dataType 保持一致
*/
private String dataType;
/**
* 参数值的数据类型dataType为非列表型intfloatdoubletextdatearray的数据规范存储在 dataSpecs
*/
private ThingModelDataSpecs dataSpecs;
/**
* 参数值的数据类型dataType为列表型enumboolstruct的数据规范存储在 dataSpecsList
*/
private List<ThingModelDataSpecs> dataSpecsList;
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelAccessModeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum;
import lombok.Data;
import java.util.List;
@ -24,13 +24,10 @@ public class ThingModelProperty {
* 属性名称
*/
private String name;
/**
* 属性描述
*/
private String description;
/**
* 云端可以对该属性进行的操作类型
* 关联枚举 {@link IotProductThingModelAccessModeEnum}
*
* 枚举 {@link IotThingModelAccessModeEnum}
*/
private String accessMode;
/**
@ -42,6 +39,8 @@ public class ThingModelProperty {
private Boolean required;
/**
* 数据类型 dataSpecs dataType 保持一致
*
* 枚举 {@link cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum}
*/
private String dataType;
/**

View File

@ -1,9 +1,15 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelArgument;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceCallTypeEnum;
import lombok.Data;
import java.util.List;
/**
* 物模型中的服务
*
* @author HUIHUI
*/
@Data
public class ThingModelService {
@ -16,18 +22,30 @@ public class ThingModelService {
*/
private String name;
/**
* 服务描述
* 是否是标准品类的必选服务
*/
private String description;
private Boolean required;
/**
* 调用类型
*
* "sync""async"
* 枚举 {@link IotThingModelServiceCallTypeEnum}
*/
private String callType;
private List<ThingModelArgument> inputData;
private List<ThingModelArgument> outputData;
/**
* 服务的输入参数
*
* 输入参数定义服务调用时所需提供的信息用于控制设备行为或执行特定任务
*/
private List<ThingModelParam> inputParams;
/**
* 服务的输出参数
*
* 输出参数定义服务调用后返回的结果或反馈信息用于确认操作结果或提供额外的信息
*/
private List<ThingModelParam> outputParams;
/**
* 标识设备需要执行的具体操作
*/
private String method;
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ThingModelArgument {
public static final String DIRECTION_INPUT = "input";
public static final String DIRECTION_OUTPUT = "output";
private String identifier;
private String name;
/**
* 物模型中的属性
*/
private ThingModelProperty property;
/**
* 用于区分输入或输出参数"input" "output"
*/
private String direction;
private String description;
}

View File

@ -20,7 +20,7 @@ public class ThingModelDateOrTextDataSpecs extends ThingModelDataSpecs {
* 数据长度单位为字节取值不能超过 2048
* dataType text 需传入该参数
*/
private Long length;
private Integer length;
/**
* 默认值可选参数用于存储默认值
*/

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelAccessModeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -25,20 +25,14 @@ public class ThingModelStructDataSpecs extends ThingModelDataSpecs {
* 属性名称
*/
private String name;
/**
* 属性描述
*/
private String description;
/**
* 云端可以对该属性进行的操作类型
* 关联枚举 {@link IotProductThingModelAccessModeEnum}
*
* 枚举 {@link IotThingModelAccessModeEnum}
*/
private String accessMode;
/**
* 是否是标准品类的必选服务
*
* - true
* - false
* 是否是标准品类的必选服务
*/
private Boolean required;
/**

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -13,7 +13,7 @@ import lombok.ToString;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IotProductThingModelPageReqVO extends PageParam {
public class IotThingModelPageReqVO extends PageParam {
@Schema(description = "功能标识")
private String identifier;
@ -22,7 +22,7 @@ public class IotProductThingModelPageReqVO extends PageParam {
private String name;
@Schema(description = "功能类型", example = "1")
@InEnum(IotProductThingModelTypeEnum.class)
@InEnum(IotThingModelTypeEnum.class)
private Integer type;
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@ -13,7 +13,7 @@ import java.time.LocalDateTime;
@Schema(description = "管理后台 - IoT 产品物模型 Response VO")
@Data
@ExcelIgnoreUnannotated
public class IotProductThingModelRespVO {
public class IotThingModelRespVO {
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816")
@ExcelProperty("产品ID")

View File

@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
@ -12,7 +12,7 @@ import lombok.Data;
@Schema(description = "管理后台 - IoT 产品物模型新增/修改 Request VO")
@Data
public class IotProductThingModelSaveReqVO {
public class IotThingModelSaveReqVO {
@Schema(description = "编号", example = "1")
private Long id;
@ -38,7 +38,7 @@ public class IotProductThingModelSaveReqVO {
@Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "功能类型不能为空")
@InEnum(IotProductThingModelTypeEnum.class)
@InEnum(IotThingModelTypeEnum.class)
private Integer type;
@Schema(description = "属性", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.iot.convert.thingmodel;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
@ -16,44 +16,44 @@ import java.util.List;
import java.util.Objects;
@Mapper
public interface IotProductThingModelConvert {
public interface IotThingModelConvert {
IotProductThingModelConvert INSTANCE = Mappers.getMapper(IotProductThingModelConvert.class);
IotThingModelConvert INSTANCE = Mappers.getMapper(IotThingModelConvert.class);
// SaveReqVO 转换为 DO
@Mapping(target = "property", expression = "java(convertToProperty(bean))")
@Mapping(target = "event", expression = "java(convertToEvent(bean))")
@Mapping(target = "service", expression = "java(convertToService(bean))")
IotProductThingModelDO convert(IotProductThingModelSaveReqVO bean);
IotThingModelDO convert(IotThingModelSaveReqVO bean);
// DO 转换为 RespVO
@Mapping(target = "property", source = "property")
@Mapping(target = "event", source = "event")
@Mapping(target = "service", source = "service")
IotProductThingModelRespVO convert(IotProductThingModelDO bean);
IotThingModelRespVO convert(IotThingModelDO bean);
// 批量转换
List<IotProductThingModelRespVO> convertList(List<IotProductThingModelDO> list);
List<IotThingModelRespVO> convertList(List<IotThingModelDO> list);
@Named("convertToProperty")
default ThingModelProperty convertToProperty(IotProductThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotProductThingModelTypeEnum.PROPERTY.getType())) {
default ThingModelProperty convertToProperty(IotThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotThingModelTypeEnum.PROPERTY.getType())) {
return bean.getProperty();
}
return null;
}
@Named("convertToEvent")
default ThingModelEvent convertToEvent(IotProductThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotProductThingModelTypeEnum.EVENT.getType())) {
default ThingModelEvent convertToEvent(IotThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotThingModelTypeEnum.EVENT.getType())) {
return bean.getEvent();
}
return null;
}
@Named("convertToService")
default ThingModelService convertToService(IotProductThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotProductThingModelTypeEnum.SERVICE.getType())) {
default ThingModelService convertToService(IotThingModelSaveReqVO bean) {
if (Objects.equals(bean.getType(), IotThingModelTypeEnum.SERVICE.getType())) {
return bean.getService();
}
return null;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.device;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -30,7 +30,7 @@ public class IotDeviceDataDO {
/**
* 物模型编号
* <p>
* 关联 {@link IotProductThingModelDO#getId()}
* 关联 {@link IotThingModelDO#getId()}
*/
private Long thingModelId;
@ -51,21 +51,21 @@ public class IotDeviceDataDO {
/**
* 属性标识符
* <p>
* 关联 {@link IotProductThingModelDO#getIdentifier()}
* 关联 {@link IotThingModelDO#getIdentifier()}
*/
private String identifier;
/**
* 属性名称
* <p>
* 关联 {@link IotProductThingModelDO#getName()}
* 关联 {@link IotThingModelDO#getName()}
*/
private String name;
/**
* 数据类型
* <p>
* 关联 {@link IotProductThingModelDO#getProperty()#getDataType()}
* 关联 {@link IotThingModelDO#getProperty()#getDataType()}
*/
private String dataType;

View File

@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -26,11 +29,10 @@ public class PluginInfoDO extends BaseDO {
*/
@TableId
private Long id;
// TODO @haohao这个是不是改成类似 key 之类的字段哈
/**
* 插件包 ID
* 插件包标识符
*/
private String pluginId;
private String pluginKey;
/**
* 插件名称
*/
@ -41,22 +43,23 @@ public class PluginInfoDO extends BaseDO {
private String description;
/**
* 部署方式
* <p>
* 枚举 {@link IotPluginDeployTypeEnum}
*/
// TODO @haohao枚举
private Integer deployType;
/**
* 插件包文件名
*/
// TODO @haohao是不是叫 fileName 避免后续有别的字段类似 fileUrl
private String file;
private String fileName;
/**
* 插件版本
*/
private String version;
/**
* 插件类型
* <p>
* 枚举 {@link IotPluginTypeEnum}
*/
// TODO @haohao枚举
private Integer type;
/**
* 设备插件协议类型
@ -64,8 +67,9 @@ public class PluginInfoDO extends BaseDO {
private String protocol;
/**
* 状态
* <p>
* 枚举 {@link IotPluginStatusEnum}
*/
// TODO @haohao枚举
private Integer status;
/**
* 插件配置项描述信息

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
// TODO @haohao一些必要的关联枚举
/**
* IoT 插件实例 DO
*
@ -33,6 +34,8 @@ public class PluginInstanceDO extends BaseDO {
private String mainId;
/**
* 插件id
* <p>
* 关联 {@link PluginInfoDO#getId()}
*/
private Long pluginId;
/**

View File

@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelR
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* FieldParser 类用于解析和转换物模型字段到 TDengine 字段
@ -15,6 +16,7 @@ public class FieldParser {
/**
* 物模型到td数据类型映射
*/
@Deprecated
private static final HashMap<String, String> TYPE_MAPPING = new HashMap<>() {
{
put("INT", "INT");
@ -52,10 +54,7 @@ public class FieldParser {
* @return 字段列表
*/
public static List<TdFieldDO> parse(ThingModelRespVO thingModel) {
// TODO @puhui999是不是使用 convertList
return thingModel.getModel().getProperties().stream()
.map(FieldParser::parse)
.collect(Collectors.toList());
return convertList(thingModel.getModel().getProperties(), FieldParser::parse);
}
/**
@ -65,18 +64,18 @@ public class FieldParser {
* @return 转换后的 TDengine 字段对象列表
*/
public static List<TdFieldDO> parse(List<List<Object>> rows) {
// TODO @puhui999是不是使用 convertList
return rows.stream().map(row -> {
return convertList(rows, row -> {
String type = row.get(1).toString().toUpperCase();
// TODO @puhui999"NCHAR" 最好枚举下
int dataLength = "NCHAR".equals(type) ? Integer.parseInt(row.get(2).toString()) : -1;
return new TdFieldDO(row.get(0).toString(), type, dataLength);
}).collect(Collectors.toList());
});
}
/**
* 获取字段字义
*/
@Deprecated
public static String getFieldDefine(TdFieldDO field) {
return "`" + field.getFieldName() + "`" + " "
+ (field.getDataLength() > 0 ? String.format("%s(%d)", field.getDataType(), field.getDataLength())

View File

@ -2,13 +2,12 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.tdengine;
import lombok.Data;
import java.util.Set;
// TODO @haohao类似这个其实可以参考 mybatis plusquerywrapper搞个 TdEngineQueryWrapper这样看起来会更好懂
/**
* 查询DO
*/
@Data
@Deprecated
public class SelectDO {
// TODO @haohadatabase 是个单词

View File

@ -6,6 +6,7 @@ import java.util.Map;
// TODO @haohao类似 SelectDO 的想法只是它是返回ps貌似可以在 tdengine 里面创建一个 query 放这种比较特殊的查询和结果对象dataobject 更多还是实际存储的结构化的 do
@Data
@Deprecated
public class SelectVisualDO {
/**

View File

@ -6,6 +6,7 @@ import lombok.Data;
* tags查询DO
*/
@Data
@Deprecated
public class TagsSelectDO {
/**

View File

@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
// TODO 芋艿看看是不是后续简化掉
/**
* TD 引擎的字段
*/
@ -12,6 +13,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Deprecated
public class TdFieldDO {
/**

View File

@ -12,6 +12,7 @@ import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Deprecated
public class TdResponse {
public static final int CODE_SUCCESS = 0;

View File

@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
*/
@Slf4j
@Service
@Deprecated // TODO 芋艿貌似没用到
public class TdRestApi {
@Value("${spring.datasource.dynamic.datasource.tdengine.url}")

View File

@ -7,6 +7,7 @@ import lombok.NoArgsConstructor;
import java.util.List;
@Deprecated
/**
* TD 引擎的数据库
*/

View File

@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelE
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@ -19,17 +19,17 @@ import lombok.NoArgsConstructor;
/**
* IoT 产品物模型功能 DO
* <p>
* 每个 {@link IotProductDO} {@link IotProductThingModelDO} 一对多的关系它的每个属性事件服务都对应一条记录
* 每个 {@link IotProductDO} {@link IotThingModelDO} 一对多的关系它的每个属性事件服务都对应一条记录
*
* @author 芋道源码
*/
@TableName(value = "iot_product_thing_model", autoResultMap = true)
@KeySequence("iot_product_thing_model_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName(value = "iot_thing_model", autoResultMap = true)
@KeySequence("iot_thing_model_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IotProductThingModelDO extends BaseDO {
public class IotThingModelDO extends BaseDO {
/**
* 物模型功能编号
@ -66,7 +66,7 @@ public class IotProductThingModelDO extends BaseDO {
/**
* 功能类型
* <p>
* 枚举 {@link IotProductThingModelTypeEnum}
* 枚举 {@link IotThingModelTypeEnum}
*/
private Integer type;

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@ -47,7 +48,8 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
}
default IotDeviceDO selectByDeviceKey(String deviceKey) {
return selectOne(IotDeviceDO::getDeviceKey, deviceKey);
return selectOne(new LambdaQueryWrapper<IotDeviceDO>()
.apply("LOWER(device_key) = {0}", deviceKey.toLowerCase()));
}
default List<IotDeviceDO> selectList(Integer deviceType) {

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.iot.dal.mysql.plugin;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.*;
/**
* IoT 插件信息 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface PluginInfoMapper extends BaseMapperX<PluginInfoDO> {
default PageResult<PluginInfoDO> selectPage(PluginInfoPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PluginInfoDO>()
.likeIfPresent(PluginInfoDO::getName, reqVO.getName())
.eqIfPresent(PluginInfoDO::getStatus, reqVO.getStatus())
.orderByDesc(PluginInfoDO::getId));
}
}

View File

@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.iot.dal.mysql.plugininstance;
package cn.iocoder.yudao.module.iot.dal.mysql.plugin;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInstancePageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.*;
/**
* IoT 插件实例 Mapper
@ -15,6 +15,12 @@ import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.*;
@Mapper
public interface PluginInstanceMapper extends BaseMapperX<PluginInstanceDO> {
default PluginInstanceDO selectByMainIdAndPluginId(String mainId, Long pluginId) {
return selectOne(new LambdaQueryWrapperX<PluginInstanceDO>()
.eq(PluginInstanceDO::getMainId, mainId)
.eq(PluginInstanceDO::getPluginId, pluginId));
}
default PageResult<PluginInstanceDO> selectPage(PluginInstancePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PluginInstanceDO>()
.eqIfPresent(PluginInstanceDO::getMainId, reqVO.getMainId())

View File

@ -1,36 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.mysql.plugininfo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.*;
/**
* IoT 插件信息 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface PluginInfoMapper extends BaseMapperX<PluginInfoDO> {
default PageResult<PluginInfoDO> selectPage(PluginInfoPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PluginInfoDO>()
.eqIfPresent(PluginInfoDO::getPluginId, reqVO.getPluginId())
.likeIfPresent(PluginInfoDO::getName, reqVO.getName())
.eqIfPresent(PluginInfoDO::getDescription, reqVO.getDescription())
.eqIfPresent(PluginInfoDO::getDeployType, reqVO.getDeployType())
.eqIfPresent(PluginInfoDO::getFile, reqVO.getFile())
.eqIfPresent(PluginInfoDO::getVersion, reqVO.getVersion())
.eqIfPresent(PluginInfoDO::getType, reqVO.getType())
.eqIfPresent(PluginInfoDO::getProtocol, reqVO.getProtocol())
.eqIfPresent(PluginInfoDO::getStatus, reqVO.getStatus())
.eqIfPresent(PluginInfoDO::getConfigSchema, reqVO.getConfigSchema())
.eqIfPresent(PluginInfoDO::getConfig, reqVO.getConfig())
.eqIfPresent(PluginInfoDO::getScript, reqVO.getScript())
.betweenIfPresent(PluginInfoDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PluginInfoDO::getId));
}
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
/**
@ -23,7 +24,8 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> {
}
default IotProductDO selectByProductKey(String productKey) {
return selectOne(IotProductDO::getProductKey, productKey);
return selectOne(new LambdaQueryWrapper<IotProductDO>()
.apply("LOWER(product_key) = {0}", productKey.toLowerCase()));
}
}

View File

@ -1,62 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.mysql.thingmodel;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* IoT 产品物模型 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface IotProductThingModelMapper extends BaseMapperX<IotProductThingModelDO> {
default PageResult<IotProductThingModelDO> selectPage(IotProductThingModelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IotProductThingModelDO>()
.eqIfPresent(IotProductThingModelDO::getIdentifier, reqVO.getIdentifier())
.likeIfPresent(IotProductThingModelDO::getName, reqVO.getName())
.eqIfPresent(IotProductThingModelDO::getType, reqVO.getType())
.eqIfPresent(IotProductThingModelDO::getProductId, reqVO.getProductId())
.notIn(IotProductThingModelDO::getIdentifier, "get", "set", "post")
.orderByDesc(IotProductThingModelDO::getId));
}
default IotProductThingModelDO selectByProductIdAndIdentifier(Long productId, String identifier) {
return selectOne(IotProductThingModelDO::getProductId, productId,
IotProductThingModelDO::getIdentifier, identifier);
}
default List<IotProductThingModelDO> selectListByProductId(Long productId) {
return selectList(IotProductThingModelDO::getProductId, productId);
}
default List<IotProductThingModelDO> selectListByProductIdAndType(Long productId, Integer type) {
return selectList(IotProductThingModelDO::getProductId, productId,
IotProductThingModelDO::getType, type);
}
default List<IotProductThingModelDO> selectListByProductIdAndIdentifiersAndTypes(Long productId,
List<String> identifiers,
List<Integer> types) {
return selectList(new LambdaQueryWrapperX<IotProductThingModelDO>()
.eq(IotProductThingModelDO::getProductId, productId)
.in(IotProductThingModelDO::getIdentifier, identifiers)
.in(IotProductThingModelDO::getType, types));
}
default IotProductThingModelDO selectByProductIdAndName(Long productId, String name) {
return selectOne(IotProductThingModelDO::getProductId, productId,
IotProductThingModelDO::getName, name);
}
default List<IotProductThingModelDO> selectListByProductKey(String productKey) {
return selectList(IotProductThingModelDO::getProductKey, productKey);
}
}

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.dal.mysql.thingmodel;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* IoT 产品物模型 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface IotThingModelMapper extends BaseMapperX<IotThingModelDO> {
default PageResult<IotThingModelDO> selectPage(IotThingModelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IotThingModelDO>()
.eqIfPresent(IotThingModelDO::getIdentifier, reqVO.getIdentifier())
.likeIfPresent(IotThingModelDO::getName, reqVO.getName())
.eqIfPresent(IotThingModelDO::getType, reqVO.getType())
.eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId())
// TODO @芋艿看看要不要加枚举
.notIn(IotThingModelDO::getIdentifier, "get", "set", "post")
.orderByDesc(IotThingModelDO::getId));
}
default IotThingModelDO selectByProductIdAndIdentifier(Long productId, String identifier) {
return selectOne(IotThingModelDO::getProductId, productId,
IotThingModelDO::getIdentifier, identifier);
}
default List<IotThingModelDO> selectListByProductId(Long productId) {
return selectList(IotThingModelDO::getProductId, productId);
}
default List<IotThingModelDO> selectListByProductIdAndType(Long productId, Integer type) {
return selectList(IotThingModelDO::getProductId, productId,
IotThingModelDO::getType, type);
}
default List<IotThingModelDO> selectListByProductIdAndIdentifiersAndTypes(Long productId,
List<String> identifiers,
List<Integer> types) {
return selectList(new LambdaQueryWrapperX<IotThingModelDO>()
.eq(IotThingModelDO::getProductId, productId)
.in(IotThingModelDO::getIdentifier, identifiers)
.in(IotThingModelDO::getType, types));
}
default IotThingModelDO selectByProductIdAndName(Long productId, String name) {
return selectOne(IotThingModelDO::getProductId, productId,
IotThingModelDO::getName, name);
}
default List<IotThingModelDO> selectListByProductKey(String productKey) {
return selectList(IotThingModelDO::getProductKey, productKey);
}
}

View File

@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.iot.dal.tdengine;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
@TDengineDS
@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析因为 JSqlParser TDengine SQL 解析会报错
public interface IotDevicePropertyDataMapper {
List<TDengineTableField> getProductPropertySTableFieldList(@Param("productKey") String productKey);
void createProductPropertySTable(@Param("productKey") String productKey,
@Param("fields") List<TDengineTableField> fields);
@SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性
default void alterProductPropertySTable(String productKey,
List<TDengineTableField> oldFields,
List<TDengineTableField> newFields) {
oldFields.removeIf(field -> TDengineTableField.FIELD_TS.equals(field.getField())
|| TDengineTableField.FIELD_DEVICE_KEY.equals(field.getField()));
List<TDengineTableField> addFields = newFields.stream().filter( // 新增的字段
newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField())))
.collect(Collectors.toList());
List<TDengineTableField> dropFields = oldFields.stream().filter( // 删除的字段
oldField -> newFields.stream().noneMatch(n -> n.getField().equals(oldField.getField())))
.collect(Collectors.toList());
List<TDengineTableField> modifyTypeFields = new ArrayList<>(); // 变更类型的字段
List<TDengineTableField> modifyLengthFields = new ArrayList<>(); // 变更长度的字段
newFields.forEach(newField -> {
TDengineTableField oldField = CollUtil.findOne(oldFields, field -> field.getField().equals(newField.getField()));
if (oldField == null) {
return;
}
if (ObjectUtil.notEqual(oldField.getType(), newField.getType())) {
modifyTypeFields.add(newField);
return;
}
if (newField.getLength()!= null) {
if (newField.getLength() > oldField.getLength()) {
modifyLengthFields.add(newField);
} else if (newField.getLength() < oldField.getLength()) {
// 特殊TDengine 长度修改时只允许变长所以此时认为是修改类型
modifyTypeFields.add(newField);
}
}
});
// 执行
addFields.forEach(field -> alterProductPropertySTableAddField(productKey, field));
dropFields.forEach(field -> alterProductPropertySTableDropField(productKey, field));
modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productKey, field));
modifyTypeFields.forEach(field -> {
alterProductPropertySTableDropField(productKey, field);
alterProductPropertySTableAddField(productKey, field);
});
}
void alterProductPropertySTableAddField(@Param("productKey") String productKey,
@Param("field") TDengineTableField field);
void alterProductPropertySTableModifyField(@Param("productKey") String productKey,
@Param("field") TDengineTableField field);
void alterProductPropertySTableDropField(@Param("productKey") String productKey,
@Param("field") TDengineTableField field);
}

View File

@ -96,7 +96,6 @@ public interface TdEngineDDLMapper {
@TenantIgnore
void modifyColumnWidthForSuperTable(TdTableDO superTable);
/**
* 修改超级表 - 为超级表添加标签
*

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.iot.emq.service;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceDataService;
import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
@ -21,7 +20,7 @@ import org.springframework.stereotype.Service;
public class EmqxServiceImpl implements EmqxService {
@Resource
private IotDeviceDataService iotDeviceDataService;
private IotDevicePropertyDataService iotDeviceDataService;
// TODO 多线程处理消息
@Override

View File

@ -1,34 +0,0 @@
package cn.iocoder.yudao.module.iot.framework.plugin;
import cn.iocoder.yudao.module.iot.api.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")
public Object serviceRegistryInitializedMarker() {
// 返回任意对象即可这里返回null都可以但最好返回个实际对象
return new Object();
}
}

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.iot.framework.plugin;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
@Configuration
public class SpringConfiguration {
@Bean
@DependsOn("serviceRegistryInitializedMarker")
public SpringPluginManager pluginManager() {
return new SpringPluginManager();
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.framework.plugin;
import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.framework.plugin.listener.CustomPluginStateListener;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.annotation.Resource;
@Slf4j
@Configuration
public class UnifiedConfiguration {
private static final String SERVICE_REGISTRY_INITIALIZED_MARKER = "serviceRegistryInitializedMarker";
@Resource
private DeviceDataApi deviceDataApi;
@Bean(SERVICE_REGISTRY_INITIALIZED_MARKER)
public Object serviceRegistryInitializedMarker() {
ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi);
log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]");
return new Object();
}
@Bean
@DependsOn(SERVICE_REGISTRY_INITIALIZED_MARKER)
public SpringPluginManager pluginManager() {
log.info("[init][实例化 SpringPluginManager]");
SpringPluginManager springPluginManager = new SpringPluginManager();
springPluginManager.addPluginStateListener(new CustomPluginStateListener());
return springPluginManager;
}
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.iot.framework.plugin.listener;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginStateListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomPluginStateListener implements PluginStateListener {
@Override
public void pluginStateChanged(PluginStateEvent event) {
// 1. 获取插件ID
String pluginId = event.getPlugin().getPluginId();
// 2. 获取插件旧状态
String oldState = event.getOldState().toString();
// 3. 获取插件新状态
String newState = event.getPluginState().toString();
// 4. 打印日志信息
log.info("插件的状态 '{}' 已更改为 '{}' 至 '{}'", pluginId, oldState, newState);
}
}

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.framework.tdengine.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* TDEngine 表字段
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TDengineTableField {
/**
* 字段名 - TDengine 默认 ts 字段默认会被 TDengine 创建
*/
public static final String FIELD_TS = "ts";
/**
* 字段名 - 我们系统定义的 device_key 字段 TDengine 默认字段
*/
public static final String FIELD_DEVICE_KEY = "device_key";
public static final String TYPE_TINYINT = "TINYINT";
public static final String TYPE_INT = "INT";
public static final String TYPE_FLOAT = "FLOAT";
public static final String TYPE_DOUBLE = "DOUBLE";
public static final String TYPE_BOOL = "BOOL";
public static final String TYPE_NCHAR = "NCHAR";
public static final String TYPE_TIMESTAMP = "TIMESTAMP";
/**
* 注释 - TAG 字段
*/
public static final String NOTE_TAG = "TAG";
/**
* 字段名
*/
private String field;
/**
* 字段类型
*/
private String type;
/**
* 字段长度
*/
private Integer length;
/**
* 注释
*/
private String note;
public TDengineTableField(String field, String type) {
this.field = field;
this.type = type;
}
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation;
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
/**
* TDEngine 数据源
*
* @author 芋道源码
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("tdengine")
public @interface TDengineDS {
}

View File

@ -0,0 +1,4 @@
/**
* iot 模块的 tdengine 拓展封装
*/
package cn.iocoder.yudao.module.iot.framework.tdengine;

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.iot.job.plugin;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.service.plugin.PluginInstanceService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 插件实例 Job
*
* @author 芋道源码
*/
@Component
public class PluginInstancesJob {
@Resource
private PluginInstanceService pluginInstanceService;
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void updatePluginInstances() {
TenantUtils.executeIgnore(() -> {
pluginInstanceService.updatePluginInstances();
});
}
}

View File

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

View File

@ -9,11 +9,18 @@ import java.util.List;
import java.util.Map;
/**
* IoT 设备数据 Service 接口
* IoT 设备属性数据 Service 接口
*
* @author 芋道源码
*/
public interface IotDeviceDataService {
public interface IotDevicePropertyDataService {
/**
* 定义设备属性数据的结构
*
* @param productId 产品编号
*/
void defineDevicePropertyData(Long productId);
/**
* 保存设备数据
@ -21,7 +28,7 @@ public interface IotDeviceDataService {
* @param productKey 产品 key
* @param deviceName 设备名称
* @param message 消息
* <p>JSON 格式参见 <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">...</a>
* <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);
@ -40,4 +47,5 @@ public interface IotDeviceDataService {
* @return 设备属性历史数据
*/
PageResult<Map<String, Object>> getHistoryDeviceProperties(@Valid IotDeviceDataPageReqVO deviceDataReqVO);
}

View File

@ -1,21 +1,29 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
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.SelectVisualDO;
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.thingmodel.IotThingModelDO;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyDataMapper;
import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO;
import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper;
import cn.iocoder.yudao.module.iot.enums.IotConstants;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.tdengine.IotThingModelMessageService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotProductThingModelService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
@ -28,11 +36,32 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
@Slf4j
/**
* IoT 设备属性数据 Service 实现类
*
* @author 芋道源码
*/
@Service
public class IotDeviceDataServiceImpl implements IotDeviceDataService {
@Slf4j
public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataService {
/**
* 物模型的数据类型 TDengine 数据类型的映射关系
*/
private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
.put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT)
.put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
.put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
.put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿为什么要映射为 TINYINT 的说明
.put( IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿为什么要映射为 TINYINT 的说明
.put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR)
.put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
.put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿怎么映射
.put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿怎么映射
.build();
@Value("${spring.datasource.dynamic.datasource.tdengine.url}")
private String url;
@ -42,13 +71,62 @@ public class IotDeviceDataServiceImpl implements IotDeviceDataService {
@Resource
private IotThingModelMessageService thingModelMessageService;
@Resource
private IotProductThingModelService thingModelService;
private IotThingModelService thingModelService;
@Resource
private IotProductService productService;
@Resource
private TdEngineDMLMapper tdEngineDMLMapper;
@Resource
private DeviceDataRedisDAO deviceDataRedisDAO;
@Resource
private IotDevicePropertyDataMapper devicePropertyDataMapper;
@Override
public void defineDevicePropertyData(Long productId) {
// 1.1 查询产品和物模型
IotProductDO product = productService.validateProductExists(productId);
List<IotThingModelDO> thingModels = filterList(thingModelService.getThingModelListByProductId(productId),
thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
// 1.2 解析 DB 里的字段
List<TDengineTableField> oldFields = new ArrayList<>();
try {
oldFields.addAll(devicePropertyDataMapper.getProductPropertySTableFieldList(product.getProductKey()));
} catch (Exception e) {
if (!e.getMessage().contains("Table does not exist")) {
throw e;
}
}
// 2.1 情况一如果是新增的时候需要创建表
List<TDengineTableField> newFields = buildTableFieldList(thingModels);
if (CollUtil.isEmpty(oldFields)) {
if (CollUtil.isEmpty(newFields)) {
log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId);
return;
}
newFields.add(0, new TDengineTableField(TDengineTableField.FIELD_TS, TDengineTableField.TYPE_TIMESTAMP));
devicePropertyDataMapper.createProductPropertySTable(product.getProductKey(), newFields);
return;
}
// 2.2 情况二如果是修改的时候需要更新表
devicePropertyDataMapper.alterProductPropertySTable(product.getProductKey(), oldFields, newFields);
}
private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
return convertList(thingModels, thingModel -> {
TDengineTableField field = new TDengineTableField(
StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写
TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
if (thingModel.getProperty().getDataType().equals(IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength());
}
return field;
});
}
@Override
public void saveDeviceData(String productKey, String deviceName, String message) {
// 1. 根据产品 key 和设备名称获得设备信息
@ -75,8 +153,8 @@ public class IotDeviceDataServiceImpl implements IotDeviceDataService {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDevice(deviceDataReqVO.getDeviceId());
// 2. 获取设备属性最新数据
List<IotProductThingModelDO> thingModelList = thingModelService.getProductThingModelListByProductKey(device.getProductKey());
thingModelList = filterList(thingModelList, thingModel -> IotProductThingModelTypeEnum.PROPERTY.getType()
List<IotThingModelDO> thingModelList = thingModelService.getProductThingModelListByProductKey(device.getProductKey());
thingModelList = filterList(thingModelList, thingModel -> IotThingModelTypeEnum.PROPERTY.getType()
.equals(thingModel.getType()));
// 3. 过滤标识符和属性名称

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
@ -60,9 +61,11 @@ public class IotDeviceServiceImpl implements IotDeviceService {
throw exception(PRODUCT_NOT_EXISTS);
}
// 1.2 校验设备标识是否唯一
if (deviceMapper.selectByDeviceKey(createReqVO.getDeviceKey()) != null) {
throw exception(DEVICE_KEY_EXISTS);
}
TenantUtils.executeIgnore(() -> {
if (deviceMapper.selectByDeviceKey(createReqVO.getDeviceKey()) != null) {
throw exception(PRODUCT_KEY_EXISTS);
}
});
// 1.3 校验设备名称在同一产品下是否唯一
if (deviceMapper.selectByProductKeyAndDeviceName(product.getProductKey(), createReqVO.getDeviceKey()) != null) {
throw exception(DEVICE_NAME_EXISTS);

View File

@ -1,12 +1,14 @@
package cn.iocoder.yudao.module.iot.service.plugininfo;
package cn.iocoder.yudao.module.iot.service.plugin;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* IoT 插件信息 Service 接口
*
@ -58,7 +60,7 @@ public interface PluginInfoService {
* @param id 插件id
* @param file 文件
*/
void uploadJar(Long id, MultipartFile file);
void uploadFile(Long id, MultipartFile file);
/**
* 更新插件的状态
@ -67,4 +69,11 @@ public interface PluginInfoService {
* @param status 状态
*/
void updatePluginStatus(Long id, Integer status);
/**
* 获得插件信息列表
*
* @return 插件信息列表
*/
List<PluginInfoDO> getPluginInfoList();
}

View File

@ -0,0 +1,270 @@
package cn.iocoder.yudao.module.iot.service.plugin;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInfoMapper;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginDescriptor;
import org.pf4j.PluginState;
import org.pf4j.PluginWrapper;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
/**
* IoT 插件信息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class PluginInfoServiceImpl implements PluginInfoService {
@Resource
private PluginInfoMapper pluginInfoMapper;
@Resource
private SpringPluginManager pluginManager;
@Value("${pf4j.pluginsDir}")
private String pluginsDir;
@Override
public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
// 插入
PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class);
pluginInfoMapper.insert(pluginInfo);
// 返回
return pluginInfo.getId();
}
@Override
public void updatePluginInfo(PluginInfoSaveReqVO updateReqVO) {
// 校验存在
validatePluginInfoExists(updateReqVO.getId());
// 更新
PluginInfoDO updateObj = BeanUtils.toBean(updateReqVO, PluginInfoDO.class);
pluginInfoMapper.updateById(updateObj);
}
@Override
public void deletePluginInfo(Long id) {
// 校验存在
PluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
// 停止插件
if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
}
// 卸载插件
PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginKey());
if (plugin != null) {
// 查询插件是否是启动状态
if (plugin.getPluginState().equals(PluginState.STARTED)) {
// 停止插件
pluginManager.stopPlugin(plugin.getPluginId());
}
// 卸载插件
pluginManager.unloadPlugin(plugin.getPluginId());
}
// 删除
pluginInfoMapper.deleteById(id);
// 删除插件文件
Executors.newSingleThreadExecutor().submit(() -> {
try {
TimeUnit.SECONDS.sleep(1); // 等待 1 避免插件未卸载完毕
File file = new File(pluginsDir, pluginInfoDO.getFileName());
if (file.exists() && !file.delete()) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
}
} catch (InterruptedException e) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
}
});
}
private PluginInfoDO validatePluginInfoExists(Long id) {
PluginInfoDO pluginInfo = pluginInfoMapper.selectById(id);
if (pluginInfo == null) {
throw exception(PLUGIN_INFO_NOT_EXISTS);
}
return pluginInfo;
}
@Override
public PluginInfoDO getPluginInfo(Long id) {
return pluginInfoMapper.selectById(id);
}
@Override
public PageResult<PluginInfoDO> getPluginInfoPage(PluginInfoPageReqVO pageReqVO) {
return pluginInfoMapper.selectPage(pageReqVO);
}
@Override
public void uploadFile(Long id, MultipartFile file) {
// 1. 校验插件信息是否存在
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
// 2. 获取插件标识
String pluginKey = pluginInfoDo.getPluginKey();
// 3. 停止并卸载旧的插件
stopAndUnloadPlugin(pluginKey);
// 4. 上传新的插件文件
String pluginKeyNew = uploadAndLoadNewPlugin(file);
// 5. 更新插件启用状态文件
updatePluginStatusFile(pluginKeyNew, false);
// 6. 更新插件信息
updatePluginInfo(pluginInfoDo, pluginKeyNew, file);
}
// 停止并卸载旧的插件
private void stopAndUnloadPlugin(String pluginKey) {
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
if (plugin != null) {
if (plugin.getPluginState().equals(PluginState.STARTED)) {
pluginManager.stopPlugin(pluginKey); // 停止插件
}
pluginManager.unloadPlugin(pluginKey); // 卸载插件
}
}
// 上传并加载新的插件文件
private String uploadAndLoadNewPlugin(MultipartFile file) {
Path pluginsPath = Paths.get(pluginsDir);
try {
if (!Files.exists(pluginsPath)) {
Files.createDirectories(pluginsPath); // 创建插件目录
}
String filename = file.getOriginalFilename();
if (filename != null) {
Path jarPath = pluginsPath.resolve(filename);
Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
} else {
throw exception(PLUGIN_INSTALL_FAILED);
}
} catch (Exception e) {
throw exception(PLUGIN_INSTALL_FAILED);
}
}
// 更新插件状态文件
private void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) {
Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;
try {
PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginKeyNew);
if (pluginWrapper == null) {
throw exception(PLUGIN_INSTALL_FAILED);
}
String pluginInfo = pluginKeyNew + "@" + pluginWrapper.getDescriptor().getVersion();
List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)
: new ArrayList<>();
List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)
: new ArrayList<>();
if (!targetLines.contains(pluginInfo)) {
targetLines.add(pluginInfo);
Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
if (oppositeLines.contains(pluginInfo)) {
oppositeLines.remove(pluginInfo);
Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
} catch (IOException e) {
throw exception(PLUGIN_INSTALL_FAILED);
}
}
// 更新插件信息
private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) {
pluginInfoDo.setPluginKey(pluginKeyNew);
pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
pluginInfoDo.setFileName(file.getOriginalFilename());
pluginInfoDo.setScript("");
PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginKeyNew).getDescriptor();
pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());
pluginInfoDo.setVersion(pluginDescriptor.getVersion());
pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
pluginInfoMapper.updateById(pluginInfoDo);
}
@Override
public void updatePluginStatus(Long id, Integer status) {
// 1. 校验插件信息是否存在
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
// 2. 校验插件状态是否有效
if (!IotPluginStatusEnum.contains(status)) {
throw exception(PLUGIN_STATUS_INVALID);
}
// 3. 获取插件标识和插件实例
String pluginKey = pluginInfoDo.getPluginKey();
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
// 4. 根据状态更新插件
if (plugin != null) {
// 4.1 如果目标状态是运行且插件未启动则启动插件
if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
&& plugin.getPluginState() != PluginState.STARTED) {
pluginManager.startPlugin(pluginKey);
updatePluginStatusFile(pluginKey, true); // 更新插件状态文件为启用
}
// 4.2 如果目标状态是停止且插件已启动则停止插件
else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
&& plugin.getPluginState() == PluginState.STARTED) {
pluginManager.stopPlugin(pluginKey);
updatePluginStatusFile(pluginKey, false); // 更新插件状态文件为禁用
}
} else {
// 5. 插件不存在且状态为停止抛出异常
if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
throw exception(PLUGIN_STATUS_INVALID);
}
}
// 6. 更新数据库中的插件状态
pluginInfoDo.setStatus(status);
pluginInfoMapper.updateById(pluginInfoDo);
}
@Override
public List<PluginInfoDO> getPluginInfoList() {
return pluginInfoMapper.selectList(null);
}
}

View File

@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.iot.service.plugin;
/**
* IoT 插件实例 Service 接口
*
* @author 芋道源码
*/
public interface PluginInstanceService {
/**
* 更新IoT 插件实例
*/
void updatePluginInstances();
}

View File

@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.iot.service.plugin;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInstanceMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginWrapper;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* IoT 插件实例 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class PluginInstanceServiceImpl implements PluginInstanceService {
/**
* 主程序id
*/
public static final String MAIN_ID = IdUtil.fastSimpleUUID();
@Resource
private PluginInfoService pluginInfoService;
@Resource
private PluginInstanceMapper pluginInstanceMapper;
@Resource
private SpringPluginManager pluginManager;
@Value("${server.port:48080}")
private int port;
@Override
public void updatePluginInstances() {
// 1. 查询 pf4j 插件列表
List<PluginWrapper> plugins = pluginManager.getPlugins();
// 2. 查询插件信息列表
List<PluginInfoDO> pluginInfos = pluginInfoService.getPluginInfoList();
// 动态获取主程序的 IP 和端口
String mainIp = getLocalIpAddress();
// 3. 遍历插件列表并保存为插件实例
for (PluginWrapper plugin : plugins) {
String pluginKey = plugin.getPluginId();
PluginInfoDO pluginInfo = pluginInfos.stream()
.filter(pluginInfoDO -> pluginInfoDO.getPluginKey().equals(pluginKey))
.findFirst()
.orElse(null);
// 4. 如果插件信息不存在则跳过
if (pluginInfo == null) {
continue;
}
// 5. 查询插件实例
PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(MAIN_ID, pluginInfo.getId());
// 6. 如果插件实例不存在则创建
if (pluginInstance == null) {
pluginInstance = new PluginInstanceDO();
pluginInstance.setPluginId(pluginInfo.getId());
pluginInstance.setMainId(MAIN_ID);
pluginInstance.setIp(mainIp);
pluginInstance.setPort(port);
pluginInstance.setHeartbeatAt(System.currentTimeMillis());
pluginInstanceMapper.insert(pluginInstance);
} else {
// 7. 如果插件实例存在则更新
pluginInstance.setHeartbeatAt(System.currentTimeMillis());
pluginInstanceMapper.updateById(pluginInstance);
}
}
}
private String getLocalIpAddress() {
try {
List<String> ipList = NetUtil.localIpv4s().stream()
.filter(ip -> !ip.startsWith("0.0") && !ip.startsWith("127.") && !ip.startsWith("169.254") && !ip.startsWith("255.255.255.255"))
.toList();
return ipList.isEmpty() ? "127.0.0.1" : ipList.get(0);
} catch (Exception e) {
log.error("获取本地IP地址失败", e);
return "127.0.0.1"; // 默认值
}
}
}

View File

@ -1,241 +0,0 @@
package cn.iocoder.yudao.module.iot.service.plugininfo;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
import cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginDescriptor;
import org.pf4j.PluginState;
import org.pf4j.PluginWrapper;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
/**
* IoT 插件信息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class PluginInfoServiceImpl implements PluginInfoService {
@Resource
private PluginInfoMapper pluginInfoMapper;
@Resource
private SpringPluginManager pluginManager;
@Resource
private FileApi fileApi;
@Value("${pf4j.pluginsDir}")
private String pluginsDir;
@Override
public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
// 插入
PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class);
pluginInfoMapper.insert(pluginInfo);
// 返回
return pluginInfo.getId();
}
@Override
public void updatePluginInfo(PluginInfoSaveReqVO updateReqVO) {
// 校验存在
validatePluginInfoExists(updateReqVO.getId());
// 更新
PluginInfoDO updateObj = BeanUtils.toBean(updateReqVO, PluginInfoDO.class);
pluginInfoMapper.updateById(updateObj);
}
@Override
public void deletePluginInfo(Long id) {
// 校验存在
PluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
// 停止插件
if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
}
// 卸载插件
PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginId());
if (plugin != null) {
// 查询插件是否是启动状态
if (plugin.getPluginState().equals(PluginState.STARTED)) {
// 停止插件
pluginManager.stopPlugin(plugin.getPluginId());
}
// 卸载插件
pluginManager.unloadPlugin(plugin.getPluginId());
}
// 删除
pluginInfoMapper.deleteById(id);
}
private PluginInfoDO validatePluginInfoExists(Long id) {
PluginInfoDO pluginInfo = pluginInfoMapper.selectById(id);
if (pluginInfo == null) {
throw exception(PLUGIN_INFO_NOT_EXISTS);
}
return pluginInfo;
}
@Override
public PluginInfoDO getPluginInfo(Long id) {
return pluginInfoMapper.selectById(id);
}
@Override
public PageResult<PluginInfoDO> getPluginInfoPage(PluginInfoPageReqVO pageReqVO) {
return pluginInfoMapper.selectPage(pageReqVO);
}
@Override
public void uploadJar(Long id, MultipartFile file) {
// 1. 校验存在
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
// 2. 判断文件名称与插件 ID 是否匹配
String pluginId = pluginInfoDo.getPluginId();
// 3. 停止卸载旧的插件
// 3.1. 获取插件信息
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
if (plugin != null) {
// 3.2. 如果插件状态是启动的停止插件
if (plugin.getPluginState().equals(PluginState.STARTED)) {
pluginManager.stopPlugin(pluginId);
}
// 3.3. 卸载插件
pluginManager.unloadPlugin(pluginId);
}
// 4. 上传插件
String pluginIdNew;
try {
String path = fileApi.createFile(pluginsDir, IoUtil.readBytes(file.getInputStream()));
Path pluginPath = Path.of(path);
pluginIdNew = pluginManager.loadPlugin(pluginPath);
} catch (Exception e) {
throw exception(PLUGIN_INSTALL_FAILED);
}
PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);
if (pluginWrapper == null) {
throw exception(PLUGIN_INSTALL_FAILED);
}
// 5. 读取配置文件和脚本
String configJson = "";
String script = "";
try (JarFile jarFile = new JarFile(pluginWrapper.getPluginPath().toFile())) {
// 5.1 获取config文件在jar包中的路径
String configFile = "classes/config.json";
JarEntry configEntry = jarFile.getJarEntry(configFile);
if (configEntry != null) {
// 5.2 读取配置文件
configJson = IoUtil.readUtf8(jarFile.getInputStream(configEntry));
log.info("configJson:{}", configJson);
}
// 5.3 读取script.js脚本
String scriptFile = "classes/script.js";
JarEntry scriptEntity = jarFile.getJarEntry(scriptFile);
if (scriptEntity != null) {
// 5.4 读取脚本文件
script = IoUtil.readUtf8(jarFile.getInputStream(scriptEntity));
log.info("script:{}", script);
}
} catch (Exception e) {
throw exception(PLUGIN_INSTALL_FAILED);
}
pluginInfoDo.setPluginId(pluginIdNew);
pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
pluginInfoDo.setFile(file.getOriginalFilename());
pluginInfoDo.setConfigSchema(configJson);
pluginInfoDo.setScript(script);
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
pluginInfoDo.setVersion(pluginDescriptor.getVersion());
pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
pluginInfoMapper.updateById(pluginInfoDo);
}
@Override
public void updatePluginStatus(Long id, Integer status) {
// 1. 校验存在
PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
// 插件状态无效
if (!IotPluginStatusEnum.contains(status)) {
throw exception(PLUGIN_STATUS_INVALID);
}
String pluginId = pluginInfoDo.getPluginId();
PluginWrapper plugin = pluginManager.getPlugin(pluginId);
if (plugin != null) {
if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) && plugin.getPluginState() != PluginState.STARTED) {
// 启动插件
pluginManager.startPlugin(pluginId);
} else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) && plugin.getPluginState() == PluginState.STARTED) {
// 停止插件
pluginManager.stopPlugin(pluginId);
}
} else {
// 已经停止未获取到插件
if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
throw exception(PLUGIN_STATUS_INVALID);
}
}
pluginInfoDo.setStatus(status);
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);
// }
// }
// }
}

View File

@ -1,54 +0,0 @@
package cn.iocoder.yudao.module.iot.service.plugininstance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstancePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstanceSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
import jakarta.validation.Valid;
/**
* IoT 插件实例 Service 接口
*
* @author 芋道源码
*/
public interface PluginInstanceService {
/**
* 创建IoT 插件实例
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createPluginInstance(@Valid PluginInstanceSaveReqVO createReqVO);
/**
* 更新IoT 插件实例
*
* @param updateReqVO 更新信息
*/
void updatePluginInstance(@Valid PluginInstanceSaveReqVO updateReqVO);
/**
* 删除IoT 插件实例
*
* @param id 编号
*/
void deletePluginInstance(Long id);
/**
* 获得IoT 插件实例
*
* @param id 编号
* @return IoT 插件实例
*/
PluginInstanceDO getPluginInstance(Long id);
/**
* 获得IoT 插件实例分页
*
* @param pageReqVO 分页查询
* @return IoT 插件实例分页
*/
PageResult<PluginInstanceDO> getPluginInstancePage(PluginInstancePageReqVO pageReqVO);
}

View File

@ -1,70 +0,0 @@
package cn.iocoder.yudao.module.iot.service.plugininstance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstancePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.plugininstance.vo.PluginInstanceSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
import cn.iocoder.yudao.module.iot.dal.mysql.plugininstance.PluginInstanceMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
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;
/**
* IoT 插件实例 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class PluginInstanceServiceImpl implements PluginInstanceService {
@Resource
private PluginInstanceMapper pluginInstanceMapper;
@Override
public Long createPluginInstance(PluginInstanceSaveReqVO createReqVO) {
// 插入
PluginInstanceDO pluginInstance = BeanUtils.toBean(createReqVO, PluginInstanceDO.class);
pluginInstanceMapper.insert(pluginInstance);
// 返回
return pluginInstance.getId();
}
@Override
public void updatePluginInstance(PluginInstanceSaveReqVO updateReqVO) {
// 校验存在
validatePluginInstanceExists(updateReqVO.getId());
// 更新
PluginInstanceDO updateObj = BeanUtils.toBean(updateReqVO, PluginInstanceDO.class);
pluginInstanceMapper.updateById(updateObj);
}
@Override
public void deletePluginInstance(Long id) {
// 校验存在
validatePluginInstanceExists(id);
// 删除
pluginInstanceMapper.deleteById(id);
}
private void validatePluginInstanceExists(Long id) {
if (pluginInstanceMapper.selectById(id) == null) {
throw exception(PLUGIN_INSTANCE_NOT_EXISTS);
}
}
@Override
public PluginInstanceDO getPluginInstance(Long id) {
return pluginInstanceMapper.selectById(id);
}
@Override
public PageResult<PluginInstanceDO> getPluginInstancePage(PluginInstancePageReqVO pageReqVO) {
return pluginInstanceMapper.selectPage(pageReqVO);
}
}

View File

@ -2,13 +2,14 @@ package cn.iocoder.yudao.module.iot.service.product;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper;
import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
import cn.iocoder.yudao.module.iot.service.tdengine.IotThingModelMessageService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotProductThingModelService;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@ -33,19 +34,23 @@ public class IotProductServiceImpl implements IotProductService {
@Resource
private IotProductMapper productMapper;
@Resource
@Lazy // 延迟加载解决循环依赖
private IotProductThingModelService thingModelFunctionService;
@Resource
@Lazy // 延迟加载解决循环依赖
private IotThingModelMessageService thingModelMessageService;
@Resource
@Lazy // 延迟加载解决循环依赖
private IotDevicePropertyDataService devicePropertyDataService;
@Override
public Long createProduct(IotProductSaveReqVO createReqVO) {
// 1. 生成 ProductKey
if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) {
throw exception(PRODUCT_KEY_EXISTS);
}
// 1. 校验 ProductKey
TenantUtils.executeIgnore(() -> {
// 为什么忽略租户避免多个租户之间productKey 重复导致 TDengine 设备属性表重复
if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) {
throw exception(PRODUCT_KEY_EXISTS);
}
});
// 2. 插入
IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus());
@ -119,8 +124,8 @@ public class IotProductServiceImpl implements IotProductService {
// 3. 产品是发布状态
if (Objects.equals(status, IotProductStatusEnum.PUBLISHED.getStatus())) {
// 3.1 创建产品超级表数据模型
thingModelFunctionService.createSuperTableDataModel(id);
// 3.2 创建物模型日志超级表数据模型
devicePropertyDataService.defineDevicePropertyData(id);
// 3.2 创建物模型日志超级表数据模型 TODO 待定message 要不要分
thingModelMessageService.createSuperTable(id);
}
productMapper.updateById(updateObj);

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.module.iot.service.tdengine;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import java.util.List;
/**
* IoT 超级表服务负责根据物模型创建和更新超级表以及创建超级表的子表等操作
*/
public interface IotSuperTableService {
/**
* 创建超级表数据模型
*/
void createSuperTableDataModel(IotProductDO product, List<IotProductThingModelDO> thingModelList);
}

View File

@ -1,260 +0,0 @@
package cn.iocoder.yudao.module.iot.service.tdengine;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
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.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDDLMapper;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* IoT 超级表服务实现类负责根据物模型创建和更新超级表以及创建超级表的子表等操作
*/
@Service
@Slf4j
public class IotSuperTableServiceImpl implements IotSuperTableService {
@Resource
private TdEngineDDLMapper tdEngineDDLMapper;
@Value("${spring.datasource.dynamic.datasource.tdengine.url}")
private String url;
@Override
public void createSuperTableDataModel(IotProductDO product, List<IotProductThingModelDO> thingModelList) {
ThingModelRespVO thingModel = buildThingModel(product, thingModelList);
if (thingModel.getModel() == null || CollUtil.isEmpty(thingModel.getModel().getProperties())) {
log.warn("物模型属性列表为空,不创建超级表");
return;
}
String superTableName = getSuperTableName(product.getDeviceType(), product.getProductKey());
String databaseName = getDatabaseName();
List<Map<String, Object>> results = tdEngineDDLMapper.showSuperTables(new TdTableDO(databaseName, superTableName));
int tableExists = results == null || results.isEmpty() ? 0 : results.size();
if (tableExists > 0) {
updateSuperTable(thingModel, product.getDeviceType());
} else {
createSuperTable(thingModel, product.getDeviceType());
}
}
/**
* 创建超级表
*/
private void createSuperTable(ThingModelRespVO thingModel, Integer deviceType) {
// 解析物模型获取字段列表
List<TdFieldDO> schemaFields = new ArrayList<>();
schemaFields.add(TdFieldDO.builder()
.fieldName("time")
.dataType("TIMESTAMP")
.build());
schemaFields.addAll(FieldParser.parse(thingModel));
// 设置超级表的标签
List<TdFieldDO> tagsFields = List.of(
TdFieldDO.builder().fieldName("product_key").dataType("NCHAR").dataLength(64).build(),
TdFieldDO.builder().fieldName("device_key").dataType("NCHAR").dataLength(64).build(),
TdFieldDO.builder().fieldName("device_name").dataType("NCHAR").dataLength(64).build(),
TdFieldDO.builder().fieldName("device_type").dataType("INT").build()
);
// 获取超级表的名称和数据库名称
String superTableName = getSuperTableName(deviceType, thingModel.getProductKey());
String databaseName = getDatabaseName();
// 创建超级表
tdEngineDDLMapper.createSuperTable(new TdTableDO(databaseName, superTableName, schemaFields, tagsFields));
}
/**
* 更新超级表
*/
private void updateSuperTable(ThingModelRespVO thingModel, Integer deviceType) {
String superTableName = getSuperTableName(deviceType, thingModel.getProductKey());
try {
List<TdFieldDO> oldFields = getTableFields(superTableName);
List<TdFieldDO> newFields = FieldParser.parse(thingModel);
updateTableFields(superTableName, oldFields, newFields);
} catch (Exception e) {
log.error("更新物模型超级表失败: {}", e.getMessage(), e);
}
}
/**
* 获取表的字段信息
*/
private List<TdFieldDO> getTableFields(String tableName) {
List<Map<String, Object>> tableDescription = tdEngineDDLMapper.describeSuperTable(new TdTableDO(getDatabaseName(), tableName));
if (CollUtil.isEmpty(tableDescription)) {
return Collections.emptyList();
}
return tableDescription.stream()
.filter(map -> !"TAG".equals(map.get("note")))
.filter(map -> !"time".equals(map.get("field")))
.map(map -> TdFieldDO.builder()
.fieldName((String) map.get("field"))
.dataType((String) map.get("type"))
.dataLength((Integer) map.get("length"))
.build())
.collect(Collectors.toList());
}
/**
* 更新表的字段包括新增修改和删除字段
*/
private void updateTableFields(String tableName, List<TdFieldDO> oldFields, List<TdFieldDO> newFields) {
String databaseName = getDatabaseName();
// 获取新增修改删除的字段
List<TdFieldDO> addFields = getAddFields(oldFields, newFields);
List<TdFieldDO> modifyFields = getModifyFields(oldFields, newFields);
List<TdFieldDO> dropFields = getDropFields(oldFields, newFields);
// 添加新增字段
if (CollUtil.isNotEmpty(addFields)) {
for (TdFieldDO addField : addFields) {
tdEngineDDLMapper.addColumnForSuperTable(TdTableDO.builder()
.dataBaseName(databaseName)
.superTableName(tableName)
.column(addField)
.build());
}
}
// 删除旧字段
if (CollUtil.isNotEmpty(dropFields)) {
for (TdFieldDO dropField : dropFields) {
tdEngineDDLMapper.dropColumnForSuperTable(TdTableDO.builder()
.dataBaseName(databaseName)
.superTableName(tableName)
.column(dropField)
.build());
}
}
// 修改字段先删除再添加
if (CollUtil.isNotEmpty(modifyFields)) {
for (TdFieldDO modifyField : modifyFields) {
tdEngineDDLMapper.dropColumnForSuperTable(TdTableDO.builder()
.dataBaseName(databaseName)
.superTableName(tableName)
.column(modifyField)
.build());
tdEngineDDLMapper.addColumnForSuperTable(TdTableDO.builder()
.dataBaseName(databaseName)
.superTableName(tableName)
.column(modifyField)
.build());
}
}
}
/**
* 获取需要新增的字段
*/
private List<TdFieldDO> getAddFields(List<TdFieldDO> oldFields, List<TdFieldDO> newFields) {
Set<String> oldFieldNames = oldFields.stream()
.map(TdFieldDO::getFieldName)
.collect(Collectors.toSet());
return newFields.stream()
.filter(f -> !oldFieldNames.contains(f.getFieldName()))
.collect(Collectors.toList());
}
/**
* 获取需要修改的字段
*/
private List<TdFieldDO> getModifyFields(List<TdFieldDO> oldFields, List<TdFieldDO> newFields) {
Map<String, TdFieldDO> oldFieldMap = oldFields.stream()
.collect(Collectors.toMap(TdFieldDO::getFieldName, f -> f));
return newFields.stream()
.filter(f -> {
TdFieldDO oldField = oldFieldMap.get(f.getFieldName());
return oldField != null && (
!oldField.getDataType().equals(f.getDataType()) ||
!Objects.equals(oldField.getDataLength(), f.getDataLength())
);
})
.collect(Collectors.toList());
}
/**
* 获取需要删除的字段
*/
private List<TdFieldDO> getDropFields(List<TdFieldDO> oldFields, List<TdFieldDO> newFields) {
Set<String> newFieldNames = newFields.stream()
.map(TdFieldDO::getFieldName)
.collect(Collectors.toSet());
return oldFields.stream()
.filter(f -> !"time".equals(f.getFieldName()))
.filter(f -> !newFieldNames.contains(f.getFieldName()))
.collect(Collectors.toList());
}
/**
* 构建物模型
*/
private ThingModelRespVO buildThingModel(IotProductDO product, List<IotProductThingModelDO> thingModelList) {
ThingModelRespVO thingModel = new ThingModelRespVO();
thingModel.setId(product.getId());
thingModel.setProductKey(product.getProductKey());
List<ThingModelProperty> properties = thingModelList.stream()
.filter(item -> IotProductThingModelTypeEnum.PROPERTY.equals(
IotProductThingModelTypeEnum.valueOfType(item.getType())))
.map(this::buildThingModelProperty)
.collect(Collectors.toList());
ThingModelRespVO.Model model = new ThingModelRespVO.Model();
model.setProperties(properties);
thingModel.setModel(model);
return thingModel;
}
/**
* 构建物模型属性
*/
private ThingModelProperty buildThingModelProperty(IotProductThingModelDO thingModel) {
ThingModelProperty property = BeanUtil.copyProperties(thingModel, ThingModelProperty.class);
property.setDataType(thingModel.getProperty().getDataType());
return property;
}
/**
* 获取数据库名称
*/
private String getDatabaseName() {
int index = url.lastIndexOf("/");
return index != -1 ? url.substring(index + 1) : url;
}
/**
* 获取超级表名称
*/
private String getSuperTableName(Integer deviceType, String productKey) {
String prefix = switch (deviceType) {
case 1 -> "gateway_sub_";
case 2 -> "gateway_";
default -> "device_";
};
return (prefix + productKey).toLowerCase();
}
}

View File

@ -11,16 +11,16 @@ 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.thingmodel.IotThingModelDO;
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.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.enums.thingmodel.IotThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotProductThingModelService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.util.IotTdDatabaseUtils;
import jakarta.annotation.Resource;
@ -52,7 +52,7 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
private String url;
@Resource
private IotProductThingModelService iotProductThingModelService;
private IotThingModelService iotThingModelService;
@Resource
private IotDeviceService iotDeviceService;
@Resource
@ -83,7 +83,7 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
// 2. 获取设备属性并进行物模型校验过滤非物模型属性
Map<String, Object> params = thingModelMessage.dataToMap();
List<IotProductThingModelDO> thingModelList = getValidThingModelList(thingModelMessage.getProductKey());
List<IotThingModelDO> thingModelList = getValidThingModelList(thingModelMessage.getProductKey());
if (thingModelList.isEmpty()) {
return;
}
@ -102,9 +102,9 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
.build());
}
private List<IotProductThingModelDO> getValidThingModelList(String productKey) {
return filterList(iotProductThingModelService.getProductThingModelListByProductKey(productKey),
thingModel -> IotProductThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
private List<IotThingModelDO> getValidThingModelList(String productKey) {
return filterList(iotThingModelService.getProductThingModelListByProductKey(productKey),
thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
}
@Override
@ -134,21 +134,17 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
tdEngineDDLMapper.createSuperTable(new TdTableDO(databaseName, superTableName, schemaFields, tagsFields));
}
private List<IotProductThingModelDO> getValidFunctionList(String productKey) {
// TODO @puhui999使用 convertList 会好点哈
return iotProductThingModelService
.getProductThingModelListByProductKey(productKey)
.stream()
.filter(function -> IotProductThingModelTypeEnum.PROPERTY.getType().equals(function.getType()))
.toList();
private List<IotThingModelDO> getValidFunctionList(String productKey) {
return filterList(iotThingModelService.getProductThingModelListByProductKey(productKey),
thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
}
private List<TdFieldDO> filterAndCollectValidFields(Map<String, Object> params, List<IotProductThingModelDO> thingModelList, IotDeviceDO device, Long time) {
private List<TdFieldDO> filterAndCollectValidFields(Map<String, Object> params, List<IotThingModelDO> thingModelList, IotDeviceDO device, Long time) {
// 1. 获取属性标识符集合
Set<String> propertyIdentifiers = convertSet(thingModelList, IotProductThingModelDO::getIdentifier);
Set<String> propertyIdentifiers = convertSet(thingModelList, IotThingModelDO::getIdentifier);
// 2. 构建属性标识符和属性的映射
Map<String, IotProductThingModelDO> thingModelMap = convertMap(thingModelList, IotProductThingModelDO::getIdentifier);
Map<String, IotThingModelDO> thingModelMap = convertMap(thingModelList, IotThingModelDO::getIdentifier);
// 3. 过滤并收集有效的属性字段
List<TdFieldDO> schemaFieldValues = new ArrayList<>();
@ -168,21 +164,21 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
* 缓存设备属性
*
* @param device 设备信息
* @param iotProductThingModelDO 物模型属性
* @param iotThingModelDO 物模型属性
* @param val 属性值
* @param time 时间
*/
private void setDeviceDataCache(IotDeviceDO device, IotProductThingModelDO iotProductThingModelDO, Object val, Long time) {
private void setDeviceDataCache(IotDeviceDO device, IotThingModelDO iotThingModelDO, Object val, Long time) {
IotDeviceDataDO deviceData = IotDeviceDataDO.builder()
.productKey(device.getProductKey())
.deviceName(device.getDeviceName())
.identifier(iotProductThingModelDO.getIdentifier())
.identifier(iotThingModelDO.getIdentifier())
.value(val != null ? val.toString() : null)
.updateTime(DateUtil.toLocalDateTime(new Date(time)))
.deviceId(device.getId())
.thingModelId(iotProductThingModelDO.getId())
.name(iotProductThingModelDO.getName())
.dataType(iotProductThingModelDO.getProperty().getDataType())
.thingModelId(iotThingModelDO.getId())
.name(iotThingModelDO.getName())
.dataType(iotThingModelDO.getProperty().getDataType())
.build();
deviceDataRedisDAO.set(deviceData);
}

View File

@ -1,442 +0,0 @@
package cn.iocoder.yudao.module.iot.service.thingmodel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelArgument;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelArrayDataSpecs;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.convert.thingmodel.IotProductThingModelConvert;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotProductThingModelMapper;
import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelAccessModeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotProductThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.tdengine.IotSuperTableService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
/**
* IoT 产品物模型 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class IotProductThingModelServiceImpl implements IotProductThingModelService {
@Resource
private IotProductThingModelMapper productThingModelMapper;
@Resource
private IotProductService productService;
@Resource
private IotSuperTableService dbStructureDataService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createProductThingModel(IotProductThingModelSaveReqVO createReqVO) {
// 1. 校验功能标识符在同一产品下是否唯一
validateIdentifierUnique(createReqVO.getProductId(), createReqVO.getIdentifier());
// 2. 功能名称在同一产品下是否唯一
validateNameUnique(createReqVO.getProductId(), createReqVO.getName());
// 3. 系统保留字段不能用于标识符定义
validateNotDefaultEventAndService(createReqVO.getIdentifier());
// 4. 校验产品状态发布状态下不允许新增功能
validateProductStatus(createReqVO.getProductId());
// 5. 插入数据库
IotProductThingModelDO thingModel = IotProductThingModelConvert.INSTANCE.convert(createReqVO);
productThingModelMapper.insert(thingModel);
// 6. 如果创建的是属性需要更新默认的事件和服务
if (Objects.equals(createReqVO.getType(), IotProductThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey());
}
return thingModel.getId();
}
private void validateProductStatus(Long createReqVO) {
IotProductDO product = productService.getProduct(createReqVO);
if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) {
throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL);
}
}
private void validateNotDefaultEventAndService(String identifier) {
// set, get, post, property, event, time, value 是系统保留字段不能用于标识符定义
if (CollUtil.containsAny(Arrays.asList("set", "get", "post", "property", "event", "time", "value"), Collections.singletonList(identifier))) {
throw exception(THING_MODEL_IDENTIFIER_INVALID);
}
}
private void validateNameUnique(Long productId, String name) {
IotProductThingModelDO thingModel = productThingModelMapper.selectByProductIdAndName(productId, name);
if (thingModel != null) {
throw exception(THING_MODEL_NAME_EXISTS);
}
}
private void validateIdentifierUnique(Long productId, String identifier) {
IotProductThingModelDO thingModel = productThingModelMapper.selectByProductIdAndIdentifier(productId, identifier);
if (thingModel != null) {
throw exception(THING_MODEL_IDENTIFIER_EXISTS);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProductThingModel(IotProductThingModelSaveReqVO updateReqVO) {
// 1. 校验功能是否存在
validateProductThingModelMapperExists(updateReqVO.getId());
// 2. 校验功能标识符是否唯一
validateIdentifierUniqueForUpdate(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier());
// 3. 校验产品状态发布状态下不允许操作功能
validateProductStatus(updateReqVO.getProductId());
// 4. 更新数据库
IotProductThingModelDO thingModel = IotProductThingModelConvert.INSTANCE.convert(updateReqVO);
productThingModelMapper.updateById(thingModel);
// 5. 如果更新的是属性需要更新默认的事件和服务
if (Objects.equals(updateReqVO.getType(), IotProductThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey());
}
}
private void validateIdentifierUniqueForUpdate(Long id, Long productId, String identifier) {
IotProductThingModelDO thingModel = productThingModelMapper.selectByProductIdAndIdentifier(productId, identifier);
if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) {
throw exception(THING_MODEL_IDENTIFIER_EXISTS);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteProductThingModel(Long id) {
// 1. 校验功能是否存在
IotProductThingModelDO thingModel = productThingModelMapper.selectById(id);
if (thingModel == null) {
throw exception(THING_MODEL_NOT_EXISTS);
}
// 3. 校验产品状态发布状态下不允许操作功能
validateProductStatus(thingModel.getProductId());
// 2. 删除功能
productThingModelMapper.deleteById(id);
// 3. 如果删除的是属性需要更新默认的事件和服务
if (Objects.equals(thingModel.getType(), IotProductThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(thingModel.getProductId(), thingModel.getProductKey());
}
}
/**
* 校验功能是否存在
*
* @param id 功能编号
*/
private void validateProductThingModelMapperExists(Long id) {
if (productThingModelMapper.selectById(id) == null) {
throw exception(THING_MODEL_NOT_EXISTS);
}
}
@Override
public IotProductThingModelDO getProductThingModel(Long id) {
return productThingModelMapper.selectById(id);
}
@Override
public List<IotProductThingModelDO> getProductThingModelListByProductId(Long productId) {
return productThingModelMapper.selectListByProductId(productId);
}
@Override
public PageResult<IotProductThingModelDO> getProductThingModelPage(IotProductThingModelPageReqVO pageReqVO) {
return productThingModelMapper.selectPage(pageReqVO);
}
@Override
public void createSuperTableDataModel(Long productId) {
// 1. 查询产品
IotProductDO product = productService.getProduct(productId);
// 2. 查询产品的物模型功能列表
List<IotProductThingModelDO> thingModelList = productThingModelMapper.selectListByProductId(productId);
// 3. 生成 TDengine 的数据模型
dbStructureDataService.createSuperTableDataModel(product, thingModelList);
}
@Override
public List<IotProductThingModelDO> getProductThingModelListByProductKey(String productKey) {
return productThingModelMapper.selectListByProductKey(productKey);
}
/**
* 创建默认的事件和服务
*/
public void createDefaultEventsAndServices(Long productId, String productKey) {
// 1. 获取当前属性列表
List<IotProductThingModelDO> propertyList = productThingModelMapper
.selectListByProductIdAndType(productId, IotProductThingModelTypeEnum.PROPERTY.getType());
// 2. 生成新的事件和服务列表
List<IotProductThingModelDO> newThingModelList = new ArrayList<>();
// 生成属性上报事件
ThingModelEvent propertyPostEvent = generatePropertyPostEvent(propertyList);
if (propertyPostEvent != null) {
IotProductThingModelDO eventThingModel = buildEventThingModelDO(productId, productKey, propertyPostEvent);
newThingModelList.add(eventThingModel);
}
// 生成属性设置服务
ThingModelService propertySetService = generatePropertySetService(propertyList);
if (propertySetService != null) {
IotProductThingModelDO setServiceThingModel = buildServiceThingModelDO(productId, productKey, propertySetService);
newThingModelList.add(setServiceThingModel);
}
// 生成属性获取服务
ThingModelService propertyGetService = generatePropertyGetService(propertyList);
if (propertyGetService != null) {
IotProductThingModelDO getServiceThingModel = buildServiceThingModelDO(productId, productKey, propertyGetService);
newThingModelList.add(getServiceThingModel);
}
// 3. 获取数据库中的默认的旧事件和服务列表
List<IotProductThingModelDO> oldThingModelList = productThingModelMapper.selectListByProductIdAndIdentifiersAndTypes(
productId,
Arrays.asList("post", "set", "get"),
Arrays.asList(IotProductThingModelTypeEnum.EVENT.getType(), IotProductThingModelTypeEnum.SERVICE.getType())
);
// 3.1 使用 diffList 方法比较新旧列表
List<List<IotProductThingModelDO>> diffResult = diffList(oldThingModelList, newThingModelList,
// 继续使用 identifier type 进行比较这样可以准确地匹配对应的功能对象
(oldFunc, newFunc) -> Objects.equals(oldFunc.getIdentifier(), newFunc.getIdentifier())
&& Objects.equals(oldFunc.getType(), newFunc.getType()));
List<IotProductThingModelDO> createList = diffResult.get(0); // 需要新增的
List<IotProductThingModelDO> updateList = diffResult.get(1); // 需要更新的
List<IotProductThingModelDO> deleteList = diffResult.get(2); // 需要删除的
// 3.2 批量执行数据库操作
// 新增数据库中的新事件和服务列表
if (CollUtil.isNotEmpty(createList)) {
productThingModelMapper.insertBatch(createList);
}
// 更新数据库中的事件和服务列表
if (CollUtil.isNotEmpty(updateList)) {
// 首先为每个需要更新的对象设置其对应的 ID
updateList.forEach(updateFunc -> {
IotProductThingModelDO oldFunc = findThingModelByIdentifierAndType(
oldThingModelList, updateFunc.getIdentifier(), updateFunc.getType());
if (oldFunc != null) {
updateFunc.setId(oldFunc.getId());
}
});
// 过滤掉没有设置 ID 的对象
List<IotProductThingModelDO> validUpdateList = updateList.stream()
.filter(func -> func.getId() != null)
.collect(Collectors.toList());
// 执行批量更新
if (CollUtil.isNotEmpty(validUpdateList)) {
productThingModelMapper.updateBatch(validUpdateList);
}
}
// 删除数据库中的旧事件和服务列表
if (CollUtil.isNotEmpty(deleteList)) {
Set<Long> idsToDelete = CollectionUtils.convertSet(deleteList, IotProductThingModelDO::getId);
productThingModelMapper.deleteByIds(idsToDelete);
}
}
/**
* 根据标识符和类型查找功能对象
*/
private IotProductThingModelDO findThingModelByIdentifierAndType(List<IotProductThingModelDO> thingModelList,
String identifier, Integer type) {
return CollUtil.findOne(thingModelList, func ->
Objects.equals(func.getIdentifier(), identifier) && Objects.equals(func.getType(), type));
}
/**
* 构建事件功能对象
*/
private IotProductThingModelDO buildEventThingModelDO(Long productId, String productKey, ThingModelEvent event) {
return new IotProductThingModelDO()
.setProductId(productId)
.setProductKey(productKey)
.setIdentifier(event.getIdentifier())
.setName(event.getName())
.setDescription(event.getDescription())
.setType(IotProductThingModelTypeEnum.EVENT.getType())
.setEvent(event);
}
/**
* 构建服务功能对象
*/
private IotProductThingModelDO buildServiceThingModelDO(Long productId, String productKey, ThingModelService service) {
return new IotProductThingModelDO()
.setProductId(productId)
.setProductKey(productKey)
.setIdentifier(service.getIdentifier())
.setName(service.getName())
.setDescription(service.getDescription())
.setType(IotProductThingModelTypeEnum.SERVICE.getType())
.setService(service);
}
/**
* 生成属性上报事件
*/
private ThingModelEvent generatePropertyPostEvent(List<IotProductThingModelDO> propertyList) {
if (CollUtil.isEmpty(propertyList)) {
return null;
}
ThingModelEvent event = new ThingModelEvent()
.setIdentifier("post")
.setName("属性上报")
.setType("info")
.setDescription("属性上报事件")
.setMethod("thing.event.property.post");
// 将属性列表转换为事件的输出参数
List<ThingModelArgument> outputData = new ArrayList<>();
for (IotProductThingModelDO thingModel : propertyList) {
ThingModelArgument arg = new ThingModelArgument()
.setIdentifier(thingModel.getIdentifier())
.setName(thingModel.getName())
.setProperty(thingModel.getProperty())
.setDescription(thingModel.getDescription())
.setDirection("output"); // 设置为输出参数
outputData.add(arg);
}
event.setOutputData(outputData);
return event;
}
/**
* 生成属性设置服务
*/
private ThingModelService generatePropertySetService(List<IotProductThingModelDO> propertyList) {
if (propertyList == null || propertyList.isEmpty()) {
return null;
}
List<ThingModelArgument> inputData = new ArrayList<>();
for (IotProductThingModelDO thingModel : propertyList) {
ThingModelProperty property = thingModel.getProperty();
if (IotProductThingModelAccessModeEnum.READ_WRITE.getMode().equals(property.getAccessMode())) {
ThingModelArgument arg = new ThingModelArgument()
.setIdentifier(property.getIdentifier())
.setName(property.getName())
.setProperty(property)
.setDescription(property.getDescription())
.setDirection("input"); // 设置为输入参数
inputData.add(arg);
}
}
if (inputData.isEmpty()) {
// 如果没有可写属性不生成属性设置服务
return null;
}
// 属性设置服务一般不需要输出参数
return new ThingModelService()
.setIdentifier("set")
.setName("属性设置")
.setCallType("async")
.setDescription("属性设置服务")
.setMethod("thing.service.property.set")
.setInputData(inputData)
// 属性设置服务一般不需要输出参数
.setOutputData(new ArrayList<>());
}
/**
* 生成属性获取服务
*/
private ThingModelService generatePropertyGetService(List<IotProductThingModelDO> propertyList) {
if (propertyList == null || propertyList.isEmpty()) {
return null;
}
List<ThingModelArgument> outputData = new ArrayList<>();
for (IotProductThingModelDO thingModelDO : propertyList) {
ThingModelProperty property = thingModelDO.getProperty();
if (ObjectUtils.equalsAny(property.getAccessMode(),
IotProductThingModelAccessModeEnum.READ_ONLY.getMode(), IotProductThingModelAccessModeEnum.READ_WRITE.getMode())) {
ThingModelArgument arg = new ThingModelArgument()
.setIdentifier(property.getIdentifier())
.setName(property.getName())
.setProperty(property)
.setDescription(property.getDescription())
.setDirection("output"); // 设置为输出参数
outputData.add(arg);
}
}
if (outputData.isEmpty()) {
// 如果没有可读属性不生成属性获取服务
return null;
}
ThingModelService service = new ThingModelService()
.setIdentifier("get")
.setName("属性获取")
.setCallType("async")
.setDescription("属性获取服务")
.setMethod("thing.service.property.get");
// 定义输入参数属性标识符列表
ThingModelArgument inputArg = new ThingModelArgument()
.setIdentifier("properties")
.setName("属性标识符列表")
.setDescription("需要获取的属性标识符列表")
.setDirection("input"); // 设置为输入参数
// 创建数组类型元素类型为文本类型字符串TODO @puhui999: 还得研究研究
ThingModelArrayDataSpecs arrayType = new ThingModelArrayDataSpecs();
arrayType.setDataType("array");
inputArg.setProperty(new ThingModelProperty().setIdentifier(inputArg.getIdentifier()).setName(inputArg.getName())
.setDescription(inputArg.getDescription()).setDataSpecs(arrayType));
ThingModelDateOrTextDataSpecs textType = new ThingModelDateOrTextDataSpecs();
textType.setDataType("text");
inputArg.setProperty(new ThingModelProperty().setIdentifier(inputArg.getIdentifier()).setName(inputArg.getName())
.setDescription(inputArg.getDescription()).setDataSpecs(textType));
service.setInputData(Collections.singletonList(inputArg));
service.setOutputData(outputData);
return service;
}
}

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.iot.service.thingmodel;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotProductThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotProductThingModelDO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import jakarta.validation.Valid;
import java.util.List;
@ -13,7 +13,7 @@ import java.util.List;
*
* @author 芋道源码
*/
public interface IotProductThingModelService {
public interface IotThingModelService {
/**
* 创建产品物模型
@ -21,21 +21,21 @@ public interface IotProductThingModelService {
* @param createReqVO 创建信息
* @return 编号
*/
Long createProductThingModel(@Valid IotProductThingModelSaveReqVO createReqVO);
Long createThingModel(@Valid IotThingModelSaveReqVO createReqVO);
/**
* 更新产品物模型
*
* @param updateReqVO 更新信息
*/
void updateProductThingModel(@Valid IotProductThingModelSaveReqVO updateReqVO);
void updateThingModel(@Valid IotThingModelSaveReqVO updateReqVO);
/**
* 删除产品物模型
*
* @param id 编号
*/
void deleteProductThingModel(Long id);
void deleteThingModel(Long id);
/**
* 获得产品物模型
@ -43,7 +43,7 @@ public interface IotProductThingModelService {
* @param id 编号
* @return 产品物模型
*/
IotProductThingModelDO getProductThingModel(Long id);
IotThingModelDO getThingModel(Long id);
/**
* 获得产品物模型列表
@ -51,7 +51,7 @@ public interface IotProductThingModelService {
* @param productId 产品编号
* @return 产品物模型列表
*/
List<IotProductThingModelDO> getProductThingModelListByProductId(Long productId);
List<IotThingModelDO> getThingModelListByProductId(Long productId);
/**
* 获得产品物模型分页
@ -59,14 +59,7 @@ public interface IotProductThingModelService {
* @param pageReqVO 分页查询
* @return 产品物模型分页
*/
PageResult<IotProductThingModelDO> getProductThingModelPage(IotProductThingModelPageReqVO pageReqVO);
/**
* 创建超级表数据模型
*
* @param productId 产品编号
*/
void createSuperTableDataModel(Long productId);
PageResult<IotThingModelDO> getProductThingModelPage(IotThingModelPageReqVO pageReqVO);
/**
* 获得产品物模型列表
@ -74,6 +67,6 @@ public interface IotProductThingModelService {
* @param productKey 产品 Key
* @return 产品物模型列表
*/
List<IotProductThingModelDO> getProductThingModelListByProductKey(String productKey);
List<IotThingModelDO> getProductThingModelListByProductKey(String productKey);
}

View File

@ -0,0 +1,330 @@
package cn.iocoder.yudao.module.iot.service.thingmodel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelParam;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO;
import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.*;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
/**
* IoT 产品物模型 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class IotThingModelServiceImpl implements IotThingModelService {
@Resource
private IotThingModelMapper thingModelMapper;
@Resource
private IotProductService productService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createThingModel(IotThingModelSaveReqVO createReqVO) {
// 1. 校验功能标识符在同一产品下是否唯一
validateIdentifierUnique(createReqVO.getProductId(), createReqVO.getIdentifier());
// 2. 功能名称在同一产品下是否唯一
validateNameUnique(createReqVO.getProductId(), createReqVO.getName());
// 3. 系统保留字段不能用于标识符定义
validateNotDefaultEventAndService(createReqVO.getIdentifier());
// 4. 校验产品状态发布状态下不允许新增功能
validateProductStatus(createReqVO.getProductId());
// 5. 插入数据库
IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO);
thingModelMapper.insert(thingModel);
// 6. 如果创建的是属性需要更新默认的事件和服务
if (Objects.equals(createReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey());
}
// TODO @puhui999: 服务和事件的情况 method 怎么设置在前端设置还是后端设置
return thingModel.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateThingModel(IotThingModelSaveReqVO updateReqVO) {
// 1. 校验功能是否存在
validateProductThingModelMapperExists(updateReqVO.getId());
// 2. 校验功能标识符是否唯一
validateIdentifierUniqueForUpdate(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier());
// 3. 校验产品状态发布状态下不允许操作功能
validateProductStatus(updateReqVO.getProductId());
// 4. 更新数据库
IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO);
thingModelMapper.updateById(thingModel);
// 5. 如果更新的是属性需要更新默认的事件和服务
if (Objects.equals(updateReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteThingModel(Long id) {
// 1. 校验功能是否存在
IotThingModelDO thingModel = thingModelMapper.selectById(id);
if (thingModel == null) {
throw exception(THING_MODEL_NOT_EXISTS);
}
// 3. 校验产品状态发布状态下不允许操作功能
validateProductStatus(thingModel.getProductId());
// 2. 删除功能
thingModelMapper.deleteById(id);
// 3. 如果删除的是属性需要更新默认的事件和服务
if (Objects.equals(thingModel.getType(), IotThingModelTypeEnum.PROPERTY.getType())) {
createDefaultEventsAndServices(thingModel.getProductId(), thingModel.getProductKey());
}
}
@Override
public IotThingModelDO getThingModel(Long id) {
return thingModelMapper.selectById(id);
}
@Override
public List<IotThingModelDO> getThingModelListByProductId(Long productId) {
return thingModelMapper.selectListByProductId(productId);
}
@Override
public PageResult<IotThingModelDO> getProductThingModelPage(IotThingModelPageReqVO pageReqVO) {
return thingModelMapper.selectPage(pageReqVO);
}
@Override
public List<IotThingModelDO> getProductThingModelListByProductKey(String productKey) {
return thingModelMapper.selectListByProductKey(productKey);
}
/**
* 校验功能是否存在
*
* @param id 功能编号
*/
private void validateProductThingModelMapperExists(Long id) {
if (thingModelMapper.selectById(id) == null) {
throw exception(THING_MODEL_NOT_EXISTS);
}
}
private void validateIdentifierUniqueForUpdate(Long id, Long productId, String identifier) {
IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier);
if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) {
throw exception(THING_MODEL_IDENTIFIER_EXISTS);
}
}
private void validateProductStatus(Long createReqVO) {
IotProductDO product = productService.getProduct(createReqVO);
if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) {
throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL);
}
}
private void validateNotDefaultEventAndService(String identifier) {
// set, get, post, property, event, time, value 是系统保留字段不能用于标识符定义
if (CollUtil.containsAny(Arrays.asList("set", "get", "post", "property", "event", "time", "value"), Collections.singletonList(identifier))) {
throw exception(THING_MODEL_IDENTIFIER_INVALID);
}
}
private void validateNameUnique(Long productId, String name) {
IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name);
if (thingModel != null) {
throw exception(THING_MODEL_NAME_EXISTS);
}
}
private void validateIdentifierUnique(Long productId, String identifier) {
IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier);
if (thingModel != null) {
throw exception(THING_MODEL_IDENTIFIER_EXISTS);
}
}
/**
* 创建默认的事件和服务
*/
public void createDefaultEventsAndServices(Long productId, String productKey) {
// 1. 获取当前属性列表
List<IotThingModelDO> properties = thingModelMapper
.selectListByProductIdAndType(productId, IotThingModelTypeEnum.PROPERTY.getType());
// 2. 生成新的事件和服务列表
List<IotThingModelDO> newThingModels = new ArrayList<>();
// 2.1 生成属性上报事件
ThingModelEvent propertyPostEvent = generatePropertyPostEvent(properties);
if (propertyPostEvent != null) {
newThingModels.add(buildEventThingModelDO(productId, productKey, propertyPostEvent, "属性上报事件"));
}
// 2.2 生成属性设置服务
ThingModelService propertySetService = generatePropertySetService(properties);
if (propertySetService != null) {
newThingModels.add(buildServiceThingModelDO(productId, productKey, propertySetService, "属性设置服务"));
}
// 2.3 生成属性获取服务
ThingModelService propertyGetService = generatePropertyGetService(properties);
if (propertyGetService != null) {
newThingModels.add(buildServiceThingModelDO(productId, productKey, propertyGetService, "属性获取服务"));
}
// 3.1 获取数据库中的默认的旧事件和服务列表
List<IotThingModelDO> oldThingModels = thingModelMapper.selectListByProductIdAndIdentifiersAndTypes(
productId,
Arrays.asList("post", "set", "get"),
Arrays.asList(IotThingModelTypeEnum.EVENT.getType(), IotThingModelTypeEnum.SERVICE.getType())
);
// 3.2 创建默认的事件和服务
createDefaultEventsAndServices(oldThingModels, newThingModels);
}
/**
* 创建默认的事件和服务
*/
private void createDefaultEventsAndServices(List<IotThingModelDO> oldThingModels,
List<IotThingModelDO> newThingModels) {
// 使用 diffList 方法比较新旧列表
List<List<IotThingModelDO>> diffResult = diffList(oldThingModels, newThingModels,
(oldVal, newVal) -> {
// 继续使用 identifier type 进行比较这样可以准确地匹配对应的功能对象
boolean same = Objects.equals(oldVal.getIdentifier(), newVal.getIdentifier())
&& Objects.equals(oldVal.getType(), newVal.getType());
if (same) {
newVal.setId(oldVal.getId()); // 设置编号
}
return same;
});
// 批量添加修改删除
if (CollUtil.isNotEmpty(diffResult.get(0))) {
thingModelMapper.insertBatch(diffResult.get(0));
}
if (CollUtil.isNotEmpty(diffResult.get(1))) {
thingModelMapper.updateBatch(diffResult.get(1));
}
if (CollUtil.isNotEmpty(diffResult.get(2))) {
thingModelMapper.deleteByIds(convertSet(diffResult.get(2), IotThingModelDO::getId));
}
}
/**
* 构建事件功能对象
*/
private IotThingModelDO buildEventThingModelDO(Long productId, String productKey,
ThingModelEvent event, String description) {
return new IotThingModelDO().setProductId(productId).setProductKey(productKey)
.setIdentifier(event.getIdentifier()).setName(event.getName()).setDescription(description)
.setType(IotThingModelTypeEnum.EVENT.getType()).setEvent(event);
}
/**
* 构建服务功能对象
*/
private IotThingModelDO buildServiceThingModelDO(Long productId, String productKey,
ThingModelService service, String description) {
return new IotThingModelDO().setProductId(productId).setProductKey(productKey)
.setIdentifier(service.getIdentifier()).setName(service.getName()).setDescription(description)
.setType(IotThingModelTypeEnum.SERVICE.getType()).setService(service);
}
/**
* 生成属性上报事件
*/
private ThingModelEvent generatePropertyPostEvent(List<IotThingModelDO> thingModels) {
// 没有属性则不生成
if (CollUtil.isEmpty(thingModels)) {
return null;
}
// 生成属性上报事件
return new ThingModelEvent().setIdentifier("post").setName("属性上报").setMethod("thing.event.property.post")
.setType(IotThingModelServiceEventTypeEnum.INFO.getType())
.setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT));
}
/**
* 生成属性设置服务
*/
private ThingModelService generatePropertySetService(List<IotThingModelDO> thingModels) {
// 1.1 过滤出所有可写属性
thingModels = filterList(thingModels, thingModel ->
IotThingModelAccessModeEnum.READ_WRITE.getMode().equals(thingModel.getProperty().getAccessMode()));
// 1.2 没有可写属性则不生成
if (CollUtil.isEmpty(thingModels)) {
return null;
}
// 2. 生成属性设置服务
return new ThingModelService().setIdentifier("set").setName("属性设置").setMethod("thing.service.property.set")
.setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType())
.setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT))
.setOutputParams(Collections.emptyList()); // 属性设置服务一般不需要输出参数
}
/**
* 生成属性获取服务
*/
private ThingModelService generatePropertyGetService(List<IotThingModelDO> thingModels) {
// 1.1 没有属性则不生成
if (CollUtil.isEmpty(thingModels)) {
return null;
}
// 1.2 生成属性获取服务
return new ThingModelService().setIdentifier("get").setName("属性获取").setMethod("thing.service.property.get")
.setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType())
.setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT))
.setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT));
}
/**
* 构建输入/输出参数列表
*
* @param thingModels 属性列表
* @return 输入/输出参数列表
*/
private List<ThingModelParam> buildInputOutputParam(List<IotThingModelDO> thingModels,
IotThingModelParamDirectionEnum direction) {
return convertList(thingModels, thingModel ->
BeanUtils.toBean(thingModel.getProperty(), ThingModelParam.class).setParaOrder(0) // TODO @puhui999: 先搞个默认值看看怎么个事
.setDirection(direction.getDirection()));
}
}

View File

@ -1,13 +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 cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_DEVICE_NOT_EXISTS;
// TODO @芋艿可能要思索下有没更好的处理方式
// TODO @芋艿怎么改成无状态
@ -34,20 +30,17 @@ public class IotTdDatabaseUtils {
* @return 产品超级表表名
*/
public static String getProductSuperTableName(Integer deviceType, String productKey) {
// TODO @alwayssuper枚举字段不要 123不符合预期抛出异常
if (deviceType == null) {
throw exception(PRODUCT_DEVICE_NOT_EXISTS);
}
Assert.notNull(deviceType, "deviceType 不能为空");
if (IotProductDeviceTypeEnum.GATEWAY_SUB.getType().equals(deviceType)) {
return String.format(IotConstants.GATEWAY_SUB_STABLE_NAME_FORMAT, productKey).toLowerCase();
} else if (IotProductDeviceTypeEnum.GATEWAY.getType().equals(deviceType)) {
}
if (IotProductDeviceTypeEnum.GATEWAY.getType().equals(deviceType)) {
return String.format(IotConstants.GATEWAY_STABLE_NAME_FORMAT, productKey).toLowerCase();
} else if (IotProductDeviceTypeEnum.DIRECT.getType().equals(deviceType)){
}
if (IotProductDeviceTypeEnum.DIRECT.getType().equals(deviceType)){
return String.format(IotConstants.DEVICE_STABLE_NAME_FORMAT, productKey).toLowerCase();
}
else{
throw exception(PRODUCT_DEVICE_NOT_EXISTS);
}
throw new IllegalArgumentException("deviceType 不正确");
}
/**
@ -58,7 +51,6 @@ public class IotTdDatabaseUtils {
*
*/
public static String getThingModelMessageSuperTableName(String productKey) {
// TODO @alwayssuper是不是应该 + 拼接就好不用 format
return "thing_model_message_" + productKey.toLowerCase();
}
@ -70,7 +62,8 @@ public class IotTdDatabaseUtils {
* @return 物模型日志设备表名
*/
public static String getThingModelMessageDeviceTableName(String productKey, String deviceName) {
return String.format(IotConstants.THING_MODEL_MESSAGE_TABLE_NAME_FORMAT, productKey.toLowerCase(), deviceName.toLowerCase());
return String.format(IotConstants.THING_MODEL_MESSAGE_TABLE_NAME_FORMAT,
productKey.toLowerCase(), deviceName.toLowerCase());
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!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.IotDevicePropertyDataMapper">
<select id="getProductPropertySTableFieldList" resultType="cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField">
DESCRIBE product_property_${productKey}
</select>
<update id="createProductPropertySTable">
CREATE STABLE product_property_${productKey}
<foreach item="field" collection="fields" separator="," open="(" close=")">
${field.field} ${field.type}
<if test="field.length != null and field.length > 0">
(${field.length})
</if>
</foreach>
TAGS (
device_key NCHAR(50)
)
</update>
<update id="alterProductPropertySTableAddField">
ALTER STABLE product_property_${productKey}
ADD COLUMN ${field.field} ${field.type}
<if test="field.length != null and field.length > 0">
(${field.length})
</if>
</update>
<update id="alterProductPropertySTableModifyField">
ALTER STABLE product_property_${productKey}
MODIFY COLUMN ${field.field} ${field.type}
<if test="field.length != null and field.length > 0">
(${field.length})
</if>
</update>
<update id="alterProductPropertySTableDropField">
ALTER STABLE product_property_${productKey}
DROP COLUMN ${field.field}
</update>
</mapper>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!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.mysql.plugininstance.PluginInstanceMapper">
<mapper namespace="cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInstanceMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。

View File

@ -5,7 +5,6 @@
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.TdThingModelMessageMapper">
<!-- 创建物模型消息日志超级表 -->
<!-- TODO @芋艿:捉摸下字段,特别是 sys、ts 这种缩写 -->
<update id="createSuperTable">
CREATE STABLE ${dataBaseName}.${superTableName}(
ts TIMESTAMP,

View File

@ -24,6 +24,7 @@
<plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class>
<plugin.version>0.0.1</plugin.version>
<plugin.provider>ahh</plugin.provider>
<plugin.description>http-plugin-0.0.1</plugin.description>
<plugin.dependencies/>
</properties>
@ -104,6 +105,7 @@
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
<Plugin-Description>${plugin.description}</Plugin-Description>
<Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
</manifestEntries>
</archive>

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.plugin;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.iot.api.DeviceDataApi;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@ -12,12 +12,9 @@ import io.netty.util.CharsetUtil;
/**
* 基于 Netty HTTP 处理器用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理
* <p>
* 请求格式
* POST /sys/{productKey}/{deviceName}/thing/event/property/post
* 请求体为 JSON 格式数据
* <p>
* 返回结果为 JSON 格式包含统一的 codedataidmessagemethodversion 字段
*
* 1. 请求格式JSON 格式地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post
* 2. 返回结果JSON 格式包含统一的 codedataidmessagemethodversion 字段
*/
public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@ -29,10 +26,9 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
String uri = request.uri();
// 期望的路径格式: /sys/{productKey}/{deviceName}/thing/event/property/post
// 使用 "/" 拆分路径
String uri = request.uri();
String[] parts = uri.split("/");
/*
@ -52,25 +48,19 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
&& "event".equals(parts[5])
&& "property".equals(parts[6])
&& "post".equals(parts[7]);
if (!isCorrectPath) {
// 如果路径不匹配返回 404 错误
writeResponse(ctx, HttpResponseStatus.NOT_FOUND, "Not Found");
return;
}
String productKey = parts[2];
String deviceName = parts[3];
// 从请求中获取原始数据
// 从请求中获取原始数据尝试解析请求数据为 JSON 对象
String requestBody = request.content().toString(CharsetUtil.UTF_8);
// 尝试解析请求数据为 JSON 对象
JSONObject jsonData;
try {
jsonData = JSONUtil.parseObj(requestBody);
} catch (Exception e) {
// 数据不是合法的 JSON 格式返回 400 错误
JSONObject res = createResponseJson(
400,
new JSONObject(),
@ -82,8 +72,6 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
writeResponse(ctx, HttpResponseStatus.BAD_REQUEST, res.toString());
return;
}
// 获取请求中的 id 字段若不存在则为 null
String id = jsonData.getStr("id", null);
try {
@ -101,7 +89,6 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
);
writeResponse(ctx, HttpResponseStatus.OK, successRes.toString());
} catch (Exception e) {
// 保存数据过程中出现异常返回 500 错误
JSONObject errorRes = createResponseJson(
500,
new JSONObject(),
@ -128,7 +115,7 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private JSONObject createResponseJson(int code, JSONObject data, String id, String message, String method, String version) {
JSONObject res = new JSONObject();
res.set("code", code);
res.set("data", data != null ? data : new JSONObject()); // 确保 data 不为 null
res.set("data", data != null ? data : new JSONObject());
res.set("id", id);
res.set("message", message);
res.set("method", method);
@ -137,24 +124,24 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
}
/**
* 向客户端返回 HTTP 响应的辅助方法
* 向客户端返回 HTTP 响应的辅助方法
*
* @param ctx 通道上下文
* @param status HTTP 响应状态码网络层面的
* @param content 响应内容JSON 字符串或其他文本
*/
private void writeResponse(ChannelHandlerContext ctx, HttpResponseStatus status, String content) {
// 设置响应头为 JSON 类型和正确的编码
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
status,
Unpooled.copiedBuffer(content, CharsetUtil.UTF_8)
);
// 设置响应头为 JSON 类型和正确的编码
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
// 发送响应并在发送完成后关闭连接
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.iot.plugin;
import cn.iocoder.yudao.module.iot.api.DeviceDataApi;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
@ -18,18 +18,25 @@ import java.util.concurrent.Executors;
public class HttpPlugin extends Plugin {
private static final int PORT = 8092;
private final ExecutorService executorService;
private DeviceDataApi deviceDataApi; // 用于保存设备数据的 API
private ExecutorService executorService;
private DeviceDataApi deviceDataApi;
public HttpPlugin(PluginWrapper wrapper) {
super(wrapper);
this.executorService = Executors.newSingleThreadExecutor(); // 创建单线程池
// 初始化线程池
this.executorService = Executors.newSingleThreadExecutor();
}
@Override
public void start() {
log.info("HttpPlugin.start()");
// 重新初始化线程池确保它是活跃的
if (executorService.isShutdown() || executorService.isTerminated()) {
executorService = Executors.newSingleThreadExecutor();
}
// ServiceRegistry 中获取主程序暴露的 DeviceDataApi 接口实例
deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
if (deviceDataApi == null) {
@ -44,10 +51,13 @@ public class HttpPlugin extends Plugin {
@Override
public void stop() {
log.info("HttpPlugin.stop()");
executorService.shutdownNow(); // 停止线程池
// 停止线程池
executorService.shutdownNow();
}
// 启动 HTTP 服务
/**
* 启动 HTTP 服务
*/
private void startHttpServer() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
@ -56,7 +66,8 @@ public class HttpPlugin extends Plugin {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(new HttpServerCodec());
@ -64,6 +75,7 @@ public class HttpPlugin extends Plugin {
// 将从 ServiceRegistry 获取的 deviceDataApi 传入处理器
channel.pipeline().addLast(new HttpHandler(deviceDataApi));
}
});
// 绑定端口并启动服务器
@ -72,10 +84,11 @@ public class HttpPlugin extends Plugin {
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("HTTP 服务启动中断", e);
log.warn("HTTP 服务启动被中断", e);
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
}