From 1aa5ef09ccfc3f38a3b1e17604d9164d656eed8e Mon Sep 17 00:00:00 2001 From: ruanrongman Date: Tue, 5 Mar 2024 18:49:04 +0800 Subject: [PATCH] feat:influxdb and glm support --- .github/workflows/check.yml | 2 +- docs/.nojekyll | 0 docs/README.md | 35 +++ docs/_coverpage.md | 46 +++ docs/images/logo.png | Bin 0 -> 217747 bytes docs/index.html | 30 ++ pom.xml | 20 +- readme.md | 15 +- .../java/top/rslly/iot/controllers/Tool.java | 17 +- .../top/rslly/iot/dao/DataRepository.java | 23 +- .../rslly/iot/dao/ProductModelRepository.java | 2 +- .../top/rslly/iot/dao/TimeDataRepository.java | 50 +++ .../java/top/rslly/iot/models/DataEntity.java | 45 +-- .../rslly/iot/models/ProductModelEntity.java | 32 +- .../iot/models/influxdb/DataTimeEntity.java | 37 +++ .../param/prompt/ProductDataDescription.java | 10 + .../param/prompt/ProductModelDescription.java | 9 + .../rslly/iot/param/request/ProductModel.java | 2 +- .../top/rslly/iot/services/DataService.java | 6 +- .../rslly/iot/services/DataServiceImpl.java | 87 ++--- .../iot/services/ProductDataService.java | 3 + .../iot/services/ProductDataServiceImpl.java | 21 ++ .../iot/services/ProductModelService.java | 11 + .../iot/services/ProductModelServiceImpl.java | 29 +- .../rslly/iot/transfer/DealThingsModel.java | 19 +- .../rslly/iot/transfer/HookProviderImpl.java | 3 +- .../top/rslly/iot/transfer/mqtt/Mqtt.java | 9 +- .../transfer/mqtt/MqttConnectionUtils.java | 2 +- .../rslly/iot/transfer/mqtt/PushCallback.java | 45 +++ .../top/rslly/iot/utility/DataCleanAuto.java | 23 +- .../rslly/iot/utility/ai/DescriptionUtil.java | 36 +++ .../java/top/rslly/iot/utility/ai/Glm.java | 296 ++++++++++++++++++ .../rslly/iot/utility/ai/IcAiException.java | 11 + .../java/top/rslly/iot/utility/ai/Prompt.java | 59 ++++ .../iot/utility/ai/tools/ControlTool.java | 76 +++++ .../utility/influxdb/InfluxDBBaseMapper.java | 30 ++ .../iot/utility/influxdb/InfluxDBConfig.java | 44 +++ .../iot/utility/influxdb/InfluxProperty.java | 39 +++ .../influxdb/InfluxProxyMapperFactory.java | 51 +++ .../utility/influxdb/ParameterHandler.java | 54 ++++ .../iot/utility/influxdb/ProxyMapper.java | 76 +++++ .../utility/influxdb/ProxyMapperRegister.java | 155 +++++++++ .../iot/utility/influxdb/ReflectUtils.java | 56 ++++ .../iot/utility/influxdb/ano/Delete.java | 31 ++ .../iot/utility/influxdb/ano/Insert.java | 30 ++ .../rslly/iot/utility/influxdb/ano/Param.java | 29 ++ .../iot/utility/influxdb/ano/Select.java | 33 ++ .../rslly/iot/utility/influxdb/ano/Tag.java | 30 ++ .../iot/utility/influxdb/ano/TimeColumn.java | 30 ++ .../utility/influxdb/executor/Executor.java | 60 ++++ .../influxdb/executor/ExecutorImpl.java | 271 ++++++++++++++++ .../script/js/AbstractJsInvokeService.java | 5 + .../script/js/NashornJsInvokeService.java | 45 ++- src/main/resources/application.yaml | 28 +- .../top/rslly/iot/DemoApplicationTests.java | 94 +++++- 55 files changed, 2161 insertions(+), 141 deletions(-) create mode 100644 docs/.nojekyll create mode 100644 docs/README.md create mode 100644 docs/_coverpage.md create mode 100644 docs/images/logo.png create mode 100644 docs/index.html create mode 100644 src/main/java/top/rslly/iot/dao/TimeDataRepository.java create mode 100644 src/main/java/top/rslly/iot/models/influxdb/DataTimeEntity.java create mode 100644 src/main/java/top/rslly/iot/param/prompt/ProductDataDescription.java create mode 100644 src/main/java/top/rslly/iot/param/prompt/ProductModelDescription.java create mode 100644 src/main/java/top/rslly/iot/transfer/mqtt/PushCallback.java create mode 100644 src/main/java/top/rslly/iot/utility/ai/DescriptionUtil.java create mode 100644 src/main/java/top/rslly/iot/utility/ai/Glm.java create mode 100644 src/main/java/top/rslly/iot/utility/ai/IcAiException.java create mode 100644 src/main/java/top/rslly/iot/utility/ai/Prompt.java create mode 100644 src/main/java/top/rslly/iot/utility/ai/tools/ControlTool.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/InfluxDBBaseMapper.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/InfluxDBConfig.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/InfluxProperty.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/InfluxProxyMapperFactory.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ParameterHandler.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ProxyMapper.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ProxyMapperRegister.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ReflectUtils.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/Delete.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/Insert.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/Param.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/Select.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/Tag.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/ano/TimeColumn.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/executor/Executor.java create mode 100644 src/main/java/top/rslly/iot/utility/influxdb/executor/ExecutorImpl.java diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c8fe846..3d1522e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,7 +20,7 @@ jobs: architecture: x64 - name: Build with Maven - run: mvn clean install + run: mvn clean install -Dmaven.test.skip=true - name: Run style checks run: mvn spotless:check diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1eb5208 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,35 @@ +# IntelliConnect + +> 一个智慧的物联网平台. + +## 概述 +* 本项目基于springboot2.7开发,使用spring security作为安全框架 +* 配备物模型和完善的监控模块 +* 原生支持promptulate大模型框架(开发中) +* 支持多种iot协议,使用emqx exhook作为mqtt通讯,可扩展性强 +* 支持微信小程序和微信服务号 +* 使用常见的mysql和redis数据库,上手简单 +* 支持时序数据库influxdb + +## 安装运行 +* 安装mysql和redis数据库,高性能运行推荐安装时序数据库influxdb +* 安装java17环境 +* 修改配置文件application.yaml +* java -jar demo-0.0.1-SNAPSHOT.jar + +## 项目特色 +* 极简主义,层次分明,符合mvc分层结构 +* 完善的物模型抽象,使得iot开发者可以专注于业务本身 + +## 交流群 + +欢迎加入群聊一起交流讨论有关Aiot相关的话题,链接过期了可以issue或email提醒一下作者。 + +
+ +
+ + +## 贡献 + +本人正在尝试一些更加完善的抽象模式,支持更多的物联网协议和数据存储形式,如果你有更好的建议,欢迎一起讨论交流。 \ No newline at end of file diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 0000000..f7ad822 --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,46 @@ + + + + + +
InteliConnect
+ +> A Powerful iot platform. + + +

+ + + + + + + + + +

+ + +

+ +

+ + + +[GitHub](https://github.com/ruanrongman/IntelliConnect.git) +[Getting Started](/README.md) diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e45e6f9467082b596993ffff981f16bcbc08e4b2 GIT binary patch literal 217747 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 aJ`MQ5E&S5_N5Jc?*VnvO{P%x-^8W$F1Zy1t literal 0 HcmV?d00001 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..0af0cc4 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,30 @@ + + + + + Document + + + + + + + +
+ + + + + diff --git a/pom.xml b/pom.xml index 36cc682..c03d2a9 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,11 @@ 5.1.49 runtime + + + org.influxdb + influxdb-java + org.springframework.boot spring-boot-starter-data-redis @@ -57,11 +62,11 @@ 1.1.23 - + io.springfox @@ -79,6 +84,12 @@ fastjson 1.2.83 + + + org.nutz + nutz + 1.r.66 + io.jsonwebtoken @@ -151,6 +162,11 @@ junit-vintage-engine test + + cn.bigmodel.openapi + oapi-java-sdk + release-V4-2.0.0 + diff --git a/readme.md b/readme.md index fe0fdea..8e1200d 100644 --- a/readme.md +++ b/readme.md @@ -14,16 +14,21 @@

+

+ +

+ ## 概述 * 本项目基于springboot2.7开发,使用spring security作为安全框架 * 配备物模型和完善的监控模块 -* 原生支持promptulate大模型框架 +* 原生支持promptulate大模型框架(开发中) * 支持多种iot协议,使用emqx exhook作为mqtt通讯,可扩展性强 * 支持微信小程序和微信服务号 * 使用常见的mysql和redis数据库,上手简单 +* 支持时序数据库influxdb ## 安装运行 -* 安装mysql和redis数据库 +* 安装mysql和redis数据库,高性能运行推荐安装时序数据库influxdb * 安装java17环境 * 修改配置文件application.yaml * java -jar demo-0.0.1-SNAPSHOT.jar @@ -34,13 +39,13 @@ ## 交流群 -欢迎加入群聊一起交流讨论有关aiot相关的话题,链接过期了可以issue或email提醒一下作者。 +欢迎加入群聊一起交流讨论有关Aiot相关的话题,链接过期了可以issue或email提醒一下作者。
- +
## 贡献 -本人正在尝试一些更加完善的抽象模式,以更好地兼容该框架,以及外部工具的扩展使用,如果你有更好的建议,欢迎一起讨论交流。 \ No newline at end of file +本人正在尝试一些更加完善的抽象模式,支持更多的物联网协议和数据存储形式,如果你有更好的建议,欢迎一起讨论交流。 \ No newline at end of file diff --git a/src/main/java/top/rslly/iot/controllers/Tool.java b/src/main/java/top/rslly/iot/controllers/Tool.java index 0137507..d870dc1 100644 --- a/src/main/java/top/rslly/iot/controllers/Tool.java +++ b/src/main/java/top/rslly/iot/controllers/Tool.java @@ -22,13 +22,17 @@ import io.swagger.v3.oas.annotations.Operation; import org.eclipse.paho.client.mqttv3.MqttException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; import top.rslly.iot.param.request.ControlParam; import top.rslly.iot.param.request.ReadData; import top.rslly.iot.services.DataServiceImpl; import top.rslly.iot.services.HardWareServiceImpl; import top.rslly.iot.utility.RedisUtil; import top.rslly.iot.utility.RuntimeMessage; +import top.rslly.iot.utility.ai.tools.ControlTool; import top.rslly.iot.utility.result.JsonResult; import top.rslly.iot.utility.result.ResultTool; @@ -41,6 +45,8 @@ public class Tool { private DataServiceImpl dataService; @Autowired private HardWareServiceImpl hardWareService; + @Autowired + private ControlTool controlTool; @Operation(summary = "用于获取平台运行环境信息", description = "单位为百分比") @RequestMapping(value = "/machineMessage", method = RequestMethod.GET) @@ -60,4 +66,13 @@ public JsonResult readData(@RequestBody ReadData readData) { return dataService.findAllByTimeBetweenAndDeviceName(readData.getTime1(), readData.getTime2(), readData.getName()); } + + @Operation(summary = "使用大模型控制设备", description = "响应速度取决于大模型速度") + @RequestMapping(value = "/aiControl", method = RequestMethod.POST) + public JsonResult aiControl(@RequestBody String content) { + //Glm glm = new Glm(); + //glm.jsonChat(content); + var answer=controlTool.run(content); + return ResultTool.success(answer); + } } diff --git a/src/main/java/top/rslly/iot/dao/DataRepository.java b/src/main/java/top/rslly/iot/dao/DataRepository.java index f1ae30b..f561bac 100644 --- a/src/main/java/top/rslly/iot/dao/DataRepository.java +++ b/src/main/java/top/rslly/iot/dao/DataRepository.java @@ -21,9 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.transaction.annotation.Transactional; import top.rslly.iot.models.DataEntity; @@ -40,15 +38,20 @@ List findAllByTimeBetweenAndDeviceIdAndJsonKey(long time, long time2 List findAllByTimeBetweenAndDeviceId(long time, long time2, int deviceId); - @Query(value = "SELECT * FROM data WHERE device_id =?1 ORDER BY time DESC LIMIT 1", + @Query(value = "SELECT * FROM data WHERE device_id =?1 And jsonkey=?2 ORDER BY time DESC LIMIT 1", nativeQuery = true) - List findAllBySort(int deviceId); + List findAllBySort(int deviceId, String jsonKey); @Transactional - List deleteById(int id); - - @Transactional - @Modifying - @Query(value = "DELETE from data WHERE id IN ?1", nativeQuery = true) - void deleteByList(List idList); + List deleteAllByTimeBeforeAndDeviceIdAndJsonKey(long time, int deviceId, String jsonKey); + /* + * @Transactional List deleteById(int id); + * + * @Transactional + * + * @Modifying + * + * @Query(value = "DELETE from data WHERE id IN ?1", nativeQuery = true) void + * deleteByList(List idList); + */ } diff --git a/src/main/java/top/rslly/iot/dao/ProductModelRepository.java b/src/main/java/top/rslly/iot/dao/ProductModelRepository.java index 2d97a24..76412fb 100644 --- a/src/main/java/top/rslly/iot/dao/ProductModelRepository.java +++ b/src/main/java/top/rslly/iot/dao/ProductModelRepository.java @@ -28,7 +28,7 @@ public interface ProductModelRepository extends JpaRepository { List findAllByProductId(int productId); - List findAllByType(String type); + List findAllByName(String name); List findAllById(int id); diff --git a/src/main/java/top/rslly/iot/dao/TimeDataRepository.java b/src/main/java/top/rslly/iot/dao/TimeDataRepository.java new file mode 100644 index 0000000..1d77c95 --- /dev/null +++ b/src/main/java/top/rslly/iot/dao/TimeDataRepository.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.dao; + +import top.rslly.iot.models.DataEntity; +import top.rslly.iot.utility.influxdb.InfluxDBBaseMapper; +import top.rslly.iot.utility.influxdb.ano.Delete; +import top.rslly.iot.utility.influxdb.ano.Param; +import top.rslly.iot.utility.influxdb.ano.Select; + +import java.util.List; + +public interface TimeDataRepository extends InfluxDBBaseMapper { + @Select(value = "SELECT * FROM \"data\" where time >=#{time1}ms and time <=#{time2}ms", + resultType = DataEntity.class) + List findAllByTimeBetween(@Param("time1") Long time1, @Param("time2") Long time2); + + @Select( + value = "SELECT * FROM \"data\" where time >=#{time}ms and time <=#{time2}ms and deviceId='#{deviceId}'", + resultType = DataEntity.class) + List findAllByTimeBetweenAndDeviceId(@Param("time") long time, + @Param("time2") long time2, @Param("deviceId") int deviceId); + + @Select( + value = "SELECT * FROM \"data\" where time >=#{time}ms and time <=#{time2}ms and deviceId='#{deviceId}' and jsonKey=#{jsonKey}", + resultType = DataEntity.class) + List findAllByTimeBetweenAndDeviceIdAndJsonKey(@Param("time") long time, + @Param("time2") long time2, @Param("deviceId") int deviceId, + @Param("jsonKey") String jsonKey); + @Delete(value = "DELETE FROM \"data\" where time <=#{time}ms and deviceId='#{deviceId}' and jsonKey=#{jsonKey}") + void deleteAllByTimeBeforeAndDeviceIdAndJsonKey(long time, int deviceId, + String jsonKey); +} diff --git a/src/main/java/top/rslly/iot/models/DataEntity.java b/src/main/java/top/rslly/iot/models/DataEntity.java index 1b05d47..54bfb7a 100644 --- a/src/main/java/top/rslly/iot/models/DataEntity.java +++ b/src/main/java/top/rslly/iot/models/DataEntity.java @@ -19,28 +19,34 @@ */ package top.rslly.iot.models; +import org.influxdb.annotation.Measurement; +import top.rslly.iot.utility.influxdb.ano.Tag; +import top.rslly.iot.utility.influxdb.ano.TimeColumn; + import javax.persistence.*; import java.util.Objects; @Entity @Table(name = "data", schema = "cwliot1.8", catalog = "") +@Measurement(name = "data") public class DataEntity { - private int id; + @TimeColumn + private long time; + @Tag private int deviceId; + @Tag private String jsonKey; private String value; private String characteristic; - private long time; @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - public int getId() { - return id; + @Column(name = "time") + public long getTime() { + return time; } - public void setId(int id) { - this.id = id; + public void setTime(long time) { + this.time = time; } @Basic @@ -83,16 +89,6 @@ public void setCharacteristic(String characteristic) { this.characteristic = characteristic; } - @Basic - @Column(name = "time") - public long getTime() { - return time; - } - - public void setTime(long time) { - this.time = time; - } - @Override public boolean equals(Object o) { if (this == o) @@ -100,13 +96,18 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; DataEntity that = (DataEntity) o; - return id == that.id && deviceId == that.deviceId && time == that.time - && Objects.equals(jsonKey, that.jsonKey) && Objects.equals(value, that.value) - && Objects.equals(characteristic, that.characteristic); + return time == that.time && deviceId == that.deviceId && Objects.equals(jsonKey, that.jsonKey) + && Objects.equals(value, that.value) && Objects.equals(characteristic, that.characteristic); } @Override public int hashCode() { - return Objects.hash(id, deviceId, jsonKey, value, characteristic, time); + return Objects.hash(time, deviceId, jsonKey, value, characteristic); + } + + @Override + public String toString() { + return "DataEntity{" + "time=" + time + ", deviceId=" + deviceId + ", jsonKey='" + jsonKey + + '\'' + ", value='" + value + '\'' + ", characteristic='" + characteristic + '\'' + '}'; } } diff --git a/src/main/java/top/rslly/iot/models/ProductModelEntity.java b/src/main/java/top/rslly/iot/models/ProductModelEntity.java index 2a19c9c..2edf687 100644 --- a/src/main/java/top/rslly/iot/models/ProductModelEntity.java +++ b/src/main/java/top/rslly/iot/models/ProductModelEntity.java @@ -26,10 +26,19 @@ @Table(name = "product_model", schema = "cwliot1.8", catalog = "") public class ProductModelEntity { private int id; - private String type; private int productId; private String description; + private String name; + @Basic + @Column(name = "name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -42,16 +51,6 @@ public void setId(int id) { this.id = id; } - @Basic - @Column(name = "type") - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - @Basic @Column(name = "product_id") public int getProductId() { @@ -75,17 +74,14 @@ public void setDescription(String description) { @Override public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; ProductModelEntity that = (ProductModelEntity) o; - return id == that.id && productId == that.productId && Objects.equals(type, that.type) - && Objects.equals(description, that.description); + return id == that.id && productId == that.productId && Objects.equals(description, that.description) && Objects.equals(name, that.name); } @Override public int hashCode() { - return Objects.hash(id, type, productId, description); + return Objects.hash(id, name, productId, description); } } diff --git a/src/main/java/top/rslly/iot/models/influxdb/DataTimeEntity.java b/src/main/java/top/rslly/iot/models/influxdb/DataTimeEntity.java new file mode 100644 index 0000000..a42dbc2 --- /dev/null +++ b/src/main/java/top/rslly/iot/models/influxdb/DataTimeEntity.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.models.influxdb; + +import lombok.Data; +import org.influxdb.annotation.Measurement; +import top.rslly.iot.utility.influxdb.ano.Tag; +import top.rslly.iot.utility.influxdb.ano.TimeColumn; + +@Data +@Measurement(name = "data") +public class DataTimeEntity { + @TimeColumn + private Long time; + @Tag + private int deviceId; + private String jsonKey; + private String value; + private String characteristic; +} diff --git a/src/main/java/top/rslly/iot/param/prompt/ProductDataDescription.java b/src/main/java/top/rslly/iot/param/prompt/ProductDataDescription.java new file mode 100644 index 0000000..7d7c349 --- /dev/null +++ b/src/main/java/top/rslly/iot/param/prompt/ProductDataDescription.java @@ -0,0 +1,10 @@ +package top.rslly.iot.param.prompt; + +import lombok.Data; + +@Data +public class ProductDataDescription { + private String valueName; + private String description; + private String type; +} diff --git a/src/main/java/top/rslly/iot/param/prompt/ProductModelDescription.java b/src/main/java/top/rslly/iot/param/prompt/ProductModelDescription.java new file mode 100644 index 0000000..ba60911 --- /dev/null +++ b/src/main/java/top/rslly/iot/param/prompt/ProductModelDescription.java @@ -0,0 +1,9 @@ +package top.rslly.iot.param.prompt; + +import lombok.Data; + +@Data +public class ProductModelDescription { + private String name; + private String description; +} diff --git a/src/main/java/top/rslly/iot/param/request/ProductModel.java b/src/main/java/top/rslly/iot/param/request/ProductModel.java index bd1dfde..f8e4b26 100644 --- a/src/main/java/top/rslly/iot/param/request/ProductModel.java +++ b/src/main/java/top/rslly/iot/param/request/ProductModel.java @@ -25,7 +25,7 @@ @Getter @Setter public class ProductModel { - private String type; + private String name; private int productId; private String description; diff --git a/src/main/java/top/rslly/iot/services/DataService.java b/src/main/java/top/rslly/iot/services/DataService.java index b01ddfe..8be31ca 100644 --- a/src/main/java/top/rslly/iot/services/DataService.java +++ b/src/main/java/top/rslly/iot/services/DataService.java @@ -25,15 +25,15 @@ import java.util.List; public interface DataService { - DataEntity insert(DataEntity dataEntity); + void insert(DataEntity dataEntity); JsonResult findAllByTimeBetweenAndDeviceName(long time, long time2, String name); List findAllByTimeBetweenAndDeviceIdAndJsonKey(long time, long time2, int deviceId, String jsonKey); - List deleteById(int id); + void deleteAllByTimeBeforeAndDeviceIdAndJsonKey(long time, int deviceId, String jsonKey); + - void deleteByList(List idList); } diff --git a/src/main/java/top/rslly/iot/services/DataServiceImpl.java b/src/main/java/top/rslly/iot/services/DataServiceImpl.java index e5a33aa..e8658fa 100644 --- a/src/main/java/top/rslly/iot/services/DataServiceImpl.java +++ b/src/main/java/top/rslly/iot/services/DataServiceImpl.java @@ -1,6 +1,6 @@ /** * Copyright © 2023-2030 The ruanrongman Authors - * + *

* Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -8,9 +8,9 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,9 +19,11 @@ */ package top.rslly.iot.services; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import top.rslly.iot.dao.DataRepository; import top.rslly.iot.dao.ProductDeviceRepository; +import top.rslly.iot.dao.TimeDataRepository; import top.rslly.iot.models.DataEntity; import top.rslly.iot.utility.result.JsonResult; import top.rslly.iot.utility.result.ResultCode; @@ -33,43 +35,56 @@ @Service public class DataServiceImpl implements DataService { - @Resource - private DataRepository dataRepository; - @Resource - private ProductDeviceRepository deviceRepository; + @Value("${storage.database}") + private String database; - @Override - public DataEntity insert(DataEntity dataEntity) { - return dataRepository.save(dataEntity); - } + @Resource + private DataRepository dataRepository; + @Resource + private ProductDeviceRepository deviceRepository; + @Resource + private TimeDataRepository timeDataRepository; - @Override - public JsonResult findAllByTimeBetweenAndDeviceName(long time, long time2, String name) { - var productDeviceEntities = deviceRepository.findAllByName(name); - if (productDeviceEntities.isEmpty()) { - return ResultTool.fail(ResultCode.PARAM_NOT_VALID); + @Override + public void insert(DataEntity dataEntity) { + if (database.equals("influxdb")) { + timeDataRepository.insert(dataEntity); + } else + dataRepository.save(dataEntity); } - int deviceId = productDeviceEntities.get(0).getId(); - var res = dataRepository.findAllByTimeBetweenAndDeviceId(time, time2, deviceId); - if (res.isEmpty()) - return ResultTool.fail(ResultCode.PARAM_NOT_VALID); - return ResultTool.success(res); - } - @Override - public List findAllByTimeBetweenAndDeviceIdAndJsonKey(long time, long time2, - int deviceId, String jsonKey) { - return dataRepository.findAllByTimeBetweenAndDeviceIdAndJsonKey(time, time2, deviceId, jsonKey); - } + @Override + public JsonResult findAllByTimeBetweenAndDeviceName(long time, long time2, String name) { + var productDeviceEntities = deviceRepository.findAllByName(name); + if (productDeviceEntities.isEmpty()) { + return ResultTool.fail(ResultCode.PARAM_NOT_VALID); + } + int deviceId = productDeviceEntities.get(0).getId(); + List res; + if (database.equals("influxdb")) { + res = timeDataRepository.findAllByTimeBetweenAndDeviceId(time, time2, deviceId); + } else { + res = dataRepository.findAllByTimeBetweenAndDeviceId(time, time2, deviceId); + } + if (res.isEmpty()) + return ResultTool.fail(ResultCode.PARAM_NOT_VALID); + return ResultTool.success(res); + } + + @Override + public List findAllByTimeBetweenAndDeviceIdAndJsonKey(long time, long time2, + int deviceId, String jsonKey) { + if (database.equals("influxdb")) { + return timeDataRepository.findAllByTimeBetweenAndDeviceIdAndJsonKey(time,time2,deviceId,jsonKey); + }else return dataRepository.findAllByTimeBetweenAndDeviceIdAndJsonKey(time, time2, deviceId, jsonKey); + } + @Override + public void deleteAllByTimeBeforeAndDeviceIdAndJsonKey(long time, int deviceId, String jsonKey) { + if (database.equals("influxdb")) { + timeDataRepository.deleteAllByTimeBeforeAndDeviceIdAndJsonKey(time, deviceId, jsonKey); + } else dataRepository.deleteAllByTimeBeforeAndDeviceIdAndJsonKey(time, deviceId, jsonKey); + } - @Override - public List deleteById(int id) { - return dataRepository.deleteById(id); - } - @Override - public void deleteByList(List idList) { - dataRepository.deleteByList(idList); - } } diff --git a/src/main/java/top/rslly/iot/services/ProductDataService.java b/src/main/java/top/rslly/iot/services/ProductDataService.java index d473dd8..ce87eee 100644 --- a/src/main/java/top/rslly/iot/services/ProductDataService.java +++ b/src/main/java/top/rslly/iot/services/ProductDataService.java @@ -20,6 +20,7 @@ package top.rslly.iot.services; import top.rslly.iot.models.ProductDataEntity; +import top.rslly.iot.param.prompt.ProductDataDescription; import top.rslly.iot.param.request.ProductData; import top.rslly.iot.utility.result.JsonResult; @@ -30,6 +31,8 @@ public interface ProductDataService { List findAllByStorageType(String storageType); + List getDescription(int modelId); + JsonResult getProductData(); JsonResult postProductData(ProductData productData); diff --git a/src/main/java/top/rslly/iot/services/ProductDataServiceImpl.java b/src/main/java/top/rslly/iot/services/ProductDataServiceImpl.java index 79a1f13..bf5172c 100644 --- a/src/main/java/top/rslly/iot/services/ProductDataServiceImpl.java +++ b/src/main/java/top/rslly/iot/services/ProductDataServiceImpl.java @@ -20,11 +20,13 @@ package top.rslly.iot.services; import org.springframework.beans.BeanUtils; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import top.rslly.iot.dao.ProductDataRepository; import top.rslly.iot.dao.ProductModelRepository; import top.rslly.iot.models.ProductDataEntity; import top.rslly.iot.models.ProductModelEntity; +import top.rslly.iot.param.prompt.ProductDataDescription; import top.rslly.iot.param.request.ProductData; import top.rslly.iot.utility.DataSave; import top.rslly.iot.utility.result.JsonResult; @@ -33,6 +35,7 @@ import javax.annotation.Resource; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; @Service @@ -53,6 +56,24 @@ public List findAllByStorageType(String storageType) { return productDataRepository.findAllByStorageType(storageType); } + @Override + public List getDescription(int modelId) { + var result = productDataRepository.findAllByModelId(modelId); + List productDataDescriptionList = new LinkedList<>(); + if(!result.isEmpty()){ + for (var s:result){ + if(s.getrRw()==1) { + ProductDataDescription productDataDescription = new ProductDataDescription(); + productDataDescription.setValueName(s.getJsonKey()); + productDataDescription.setDescription(s.getDescription()); + productDataDescription.setType(s.getType()); + productDataDescriptionList.add(productDataDescription); + } + } + } + return productDataDescriptionList; + } + @Override public JsonResult getProductData() { var result = productDataRepository.findAll(); diff --git a/src/main/java/top/rslly/iot/services/ProductModelService.java b/src/main/java/top/rslly/iot/services/ProductModelService.java index 7454f94..dfd54c3 100644 --- a/src/main/java/top/rslly/iot/services/ProductModelService.java +++ b/src/main/java/top/rslly/iot/services/ProductModelService.java @@ -19,9 +19,14 @@ */ package top.rslly.iot.services; +import top.rslly.iot.models.ProductEntity; +import top.rslly.iot.models.ProductModelEntity; +import top.rslly.iot.param.prompt.ProductModelDescription; import top.rslly.iot.param.request.ProductModel; import top.rslly.iot.utility.result.JsonResult; +import java.util.List; + public interface ProductModelService { JsonResult getProductModel(); @@ -29,6 +34,12 @@ public interface ProductModelService { JsonResult postProductModel(ProductModel productModel); + List getDescription(int productId); + + List findAllByProductId(int productId); + + List findAllByName(String name); + JsonResult deleteProductModel(int id); } diff --git a/src/main/java/top/rslly/iot/services/ProductModelServiceImpl.java b/src/main/java/top/rslly/iot/services/ProductModelServiceImpl.java index 76faaa1..a1ecdae 100644 --- a/src/main/java/top/rslly/iot/services/ProductModelServiceImpl.java +++ b/src/main/java/top/rslly/iot/services/ProductModelServiceImpl.java @@ -25,12 +25,14 @@ import top.rslly.iot.dao.ProductRepository; import top.rslly.iot.models.ProductEntity; import top.rslly.iot.models.ProductModelEntity; +import top.rslly.iot.param.prompt.ProductModelDescription; import top.rslly.iot.param.request.ProductModel; import top.rslly.iot.utility.result.JsonResult; import top.rslly.iot.utility.result.ResultCode; import top.rslly.iot.utility.result.ResultTool; import javax.annotation.Resource; +import java.util.LinkedList; import java.util.List; @Service @@ -64,7 +66,7 @@ public JsonResult postProductModel(ProductModel productModel) { ProductModelEntity productModelEntity = new ProductModelEntity(); BeanUtils.copyProperties(productModel, productModelEntity); List result = productRepository.findAllById(productModel.getProductId()); - List p1 = productModelRepository.findAllByType(productModel.getType()); + List p1 = productModelRepository.findAllByName(productModel.getName()); if (result.isEmpty() || !p1.isEmpty()) return ResultTool.fail(ResultCode.COMMON_FAIL); else { @@ -73,6 +75,31 @@ public JsonResult postProductModel(ProductModel productModel) { } } + @Override + public List getDescription(int productId) { + var result = productModelRepository.findAllByProductId(productId); + List productModelDescriptionList = new LinkedList<>(); + if (!result.isEmpty()) { + for (var s : result) { + ProductModelDescription productModelDescription = new ProductModelDescription(); + productModelDescription.setName(s.getName()); + productModelDescription.setDescription(s.getDescription()); + productModelDescriptionList.add(productModelDescription); + } + } + return productModelDescriptionList; + } + + @Override + public List findAllByProductId(int productId) { + return productModelRepository.findAllByProductId(productId); + } + + @Override + public List findAllByName(String name) { + return productModelRepository.findAllByName(name); + } + @Override public JsonResult deleteProductModel(int id) { List result = productModelRepository.deleteById(id); diff --git a/src/main/java/top/rslly/iot/transfer/DealThingsModel.java b/src/main/java/top/rslly/iot/transfer/DealThingsModel.java index 8fa43f3..a76b4a0 100644 --- a/src/main/java/top/rslly/iot/transfer/DealThingsModel.java +++ b/src/main/java/top/rslly/iot/transfer/DealThingsModel.java @@ -30,6 +30,8 @@ import top.rslly.iot.utility.DataSave; import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Component public class DealThingsModel { @@ -40,11 +42,14 @@ public class DealThingsModel { @Autowired private DataServiceImpl dataService; + private final Lock lock = new ReentrantLock(); + + // 同步问题 @Async("taskExecutor") - public void deal(String clientId, long time, String message) { + public void deal(String clientId, String topic, String message) { String characteristic = UUID.randomUUID().toString(); var deviceEntityList = productDeviceService.findAllByClientId(clientId); - if (deviceEntityList.isEmpty()) + if (deviceEntityList.isEmpty() || !deviceEntityList.get(0).getSubscribeTopic().equals(topic)) return; int modelId = deviceEntityList.get(0).getModelId(); var productDataEntities = productDataService.findAllByModelId(modelId); @@ -58,8 +63,14 @@ public void deal(String clientId, long time, String message) { dataEntity.setJsonKey(s.getJsonKey()); dataEntity.setValue(result.toString()); dataEntity.setDeviceId(deviceEntityList.get(0).getId()); - dataEntity.setTime(time); - dataService.insert(dataEntity); + lock.lock(); + try { + long time = System.currentTimeMillis(); + dataEntity.setTime(time); + dataService.insert(dataEntity); + } finally { + lock.unlock(); + } } } } diff --git a/src/main/java/top/rslly/iot/transfer/HookProviderImpl.java b/src/main/java/top/rslly/iot/transfer/HookProviderImpl.java index 5c8d3ea..b8fc0f5 100644 --- a/src/main/java/top/rslly/iot/transfer/HookProviderImpl.java +++ b/src/main/java/top/rslly/iot/transfer/HookProviderImpl.java @@ -95,8 +95,7 @@ public void onClientDisconnected(ClientDisconnectedRequest request, @Override public void onMessageDelivered(MessageDeliveredRequest request, StreamObserver responseObserver) { - long time = System.currentTimeMillis(); - dealThingsModel.deal(request.getClientinfo().getClientid(), time, + dealThingsModel.deal(request.getClientinfo().getClientid(), request.getMessage().getTopic(), request.getMessage().getPayload().toStringUtf8()); EmptySuccess reply = EmptySuccess.newBuilder().build(); responseObserver.onNext(reply); diff --git a/src/main/java/top/rslly/iot/transfer/mqtt/Mqtt.java b/src/main/java/top/rslly/iot/transfer/mqtt/Mqtt.java index fe03621..0cd8647 100644 --- a/src/main/java/top/rslly/iot/transfer/mqtt/Mqtt.java +++ b/src/main/java/top/rslly/iot/transfer/mqtt/Mqtt.java @@ -19,6 +19,7 @@ */ package top.rslly.iot.transfer.mqtt; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -29,11 +30,11 @@ import org.springframework.stereotype.Component; +@Slf4j @Component public class Mqtt implements ApplicationRunner { - private static final Logger LOG = LoggerFactory.getLogger(Mqtt.class); private String Host; private String Username; private String Password; @@ -41,19 +42,19 @@ public class Mqtt implements ApplicationRunner { @Value("${mqtt.Host}") public void setHost(String host) { this.Host = host; - LOG.info("用户名" + host); + log.info("用户名{}", host); } @Value("${mqtt.username}") public void setUsername(String username) { this.Username = username; - LOG.info("用户名" + username); + log.info("用户名{}", username); } @Value("${mqtt.password}") public void setPassword(String password) { this.Password = password; - LOG.info("密码" + password); + log.info("密码{}", password); } @Override diff --git a/src/main/java/top/rslly/iot/transfer/mqtt/MqttConnectionUtils.java b/src/main/java/top/rslly/iot/transfer/mqtt/MqttConnectionUtils.java index 5fb31e9..4372147 100644 --- a/src/main/java/top/rslly/iot/transfer/mqtt/MqttConnectionUtils.java +++ b/src/main/java/top/rslly/iot/transfer/mqtt/MqttConnectionUtils.java @@ -81,7 +81,7 @@ public static void start(String Host, String Username, String Password) throws M // MqttTopic topic = client.getTopic(TOPIC); // setWill方法,如果项目中需要知道客户端是否掉线可以调用该方法。设置最终端口的通知消息 // connectOptions.setWill(topic, "close".getBytes(), 2, true); - + client.setCallback(new PushCallback()); log.info("WIFI版启动成功================="); } diff --git a/src/main/java/top/rslly/iot/transfer/mqtt/PushCallback.java b/src/main/java/top/rslly/iot/transfer/mqtt/PushCallback.java new file mode 100644 index 0000000..e119c6b --- /dev/null +++ b/src/main/java/top/rslly/iot/transfer/mqtt/PushCallback.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.transfer.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +@Slf4j +public class PushCallback implements MqttCallback { + @Override + public void connectionLost(Throwable throwable) { + // 连接丢失后,一般在这里面进行重连 + log.info("WIFI版======连接断开,可以做重连"); + MqttConnectionUtils.reConnect(); + } + + @Override + public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { + + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } +} diff --git a/src/main/java/top/rslly/iot/utility/DataCleanAuto.java b/src/main/java/top/rslly/iot/utility/DataCleanAuto.java index 565b123..f042784 100644 --- a/src/main/java/top/rslly/iot/utility/DataCleanAuto.java +++ b/src/main/java/top/rslly/iot/utility/DataCleanAuto.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import top.rslly.iot.models.ProductDataEntity; import top.rslly.iot.models.ProductDeviceEntity; @@ -43,12 +44,14 @@ public class DataCleanAuto { @Autowired private DataServiceImpl dataService; + @Scheduled(cron = "0 0 0 * * ?") public void task() { log.info("时间到了,执行清理任务!!!"); // 当前时间日历 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, -7); - long lastTime = calendar.getTime().getTime(); + // long lastTime = calendar.getTime().getTime(); + long lastTime = System.currentTimeMillis(); // System.out.println(lastTime); List productDataEntityList = productDataService.findAllByStorageType(DataSave.week.getStorageType()); @@ -58,16 +61,14 @@ public void task() { if (productDeviceEntityList.isEmpty()) continue; for (var device : productDeviceEntityList) { - var dataId = dataService.findAllByTimeBetweenAndDeviceIdAndJsonKey(0, lastTime, - device.getId(), s.getJsonKey()); - if (dataId.isEmpty()) - continue; - List idList = new ArrayList<>(); - for (var id : dataId) { - idList.add(id.getId()); - } - // System.out.println(dataId.get(0).getId()); - dataService.deleteByList(idList); + /* + * var dataId = dataService.findAllByTimeBetweenAndDeviceIdAndJsonKey(0, lastTime, + * device.getId(), s.getJsonKey()); if (dataId.isEmpty()) continue; List idList = + * new ArrayList<>(); for (var id : dataId) { idList.add(id.getId()); } // + * System.out.println(dataId.get(0).getId()); dataService.deleteByList(idList); + */ + dataService.deleteAllByTimeBeforeAndDeviceIdAndJsonKey(lastTime, device.getId(), + s.getJsonKey()); } } } diff --git a/src/main/java/top/rslly/iot/utility/ai/DescriptionUtil.java b/src/main/java/top/rslly/iot/utility/ai/DescriptionUtil.java new file mode 100644 index 0000000..dd00811 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/ai/DescriptionUtil.java @@ -0,0 +1,36 @@ +package top.rslly.iot.utility.ai; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.checkerframework.checker.units.qual.A; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import top.rslly.iot.param.request.ProductModel; +import top.rslly.iot.services.ProductDataServiceImpl; +import top.rslly.iot.services.ProductDeviceServiceImpl; +import top.rslly.iot.services.ProductModelServiceImpl; + +import java.util.List; + +@Component +public class DescriptionUtil { + @Autowired + private ProductModelServiceImpl productModelService; + @Autowired + private ProductDataServiceImpl productDataService; + @Autowired + private ProductDeviceServiceImpl productDeviceService; + public String GetElectricalName(){ + var result = productModelService.getDescription(1); + return JSON.toJSONString(result); + } + public String GetPropertiesAndValue(){ + JSONObject jsonObject = new JSONObject(); + var result = productModelService.findAllByProductId(1); + for (var s:result){ + var dataList=productDataService.getDescription(s.getId()); + jsonObject.put(s.getName(),dataList); + } + return jsonObject.toJSONString(); + } +} diff --git a/src/main/java/top/rslly/iot/utility/ai/Glm.java b/src/main/java/top/rslly/iot/utility/ai/Glm.java new file mode 100644 index 0000000..7f3b4c3 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/ai/Glm.java @@ -0,0 +1,296 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.ai; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONException; +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.zhipu.oapi.ClientV4; +import com.zhipu.oapi.Constants; +import com.zhipu.oapi.service.v4.model.*; +import io.reactivex.Flowable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Glm { + // 请自定义自己的业务id + private static final String requestIdTemplate = "mycompany-%d"; + private static final String API_KEY = "bed33bce4df98801350547a1a5dc2f24.kPL8lRKdIaantJss"; + + + private static final ClientV4 client = new ClientV4.Builder(API_KEY).build(); + private static final ObjectMapper mapper = defaultObjectMapper(); + private static String system_prompt = + """ + As a smart speaker, your name is Xiaochuang, developed by the Chuangwanlian team. You are good at helping people operate various appliances, + You can only control the following electrical:{"lamp","pump","air_conditioner"} + lamp properties:{"switch","bright","color"} + pump properties:{"switch"} + air_conditioner properties:{"switch","temp"} + lamp properties value: + switch args:{"type":"string","description":"on/off"}, + bright args: {"type":"int","description":"controls the brightness(0-100) of the light"}, + color args: {"type":"string","description":"use 0xffffff code to control it"} + pump properties value: + switch args:{"type":"string","description":"on/off"}, + air_conditioner value: + switch args:{"type":"string","description":"on/off"}, + temp args:{"type":"float","description":"to control the temperature"} + ## Output Format + To answer the question, Use the following JSON format. JSON only, no explanation. Otherwise, you will be punished. + The output should be formatted as a JSON instance that conforms to the format below. JSON only, no explanation. + + ```json + { + "thought": "The thought of what to do and why.(use chinese)", + "action": # the action to take, must be one of provided tools + { + "name": "electronic name,If it doesn't match, please output null", + "code": "if success output 200,else output 400", + "answer": "Answer humanity,Be polite and gentle as much as possible", + "properties": "electronic input parameters, json list data,If there is no such properties, please output []", + "value": "electronic input parameters, json list data like ["on",30],If it doesn't match, please output []", + } + } + ``` + If you find that the appliance and properties that the user wants to control is not in the above range, please return it in the following format + ```json + { + "thought": "The thought of what to do and why.", + "action": # the action to take, must be one of provided tools + { + "name": "null", + "code": "400", + "answer": "Answer humanity,Be polite and gentle as much as possible", + "properties": [], + "value": [], + } + } + ``` + """; + private static String system_prompt2 = """ + As a smart speaker, your name is 小创, developed by the 创万联 team. You are good at helping people operate various appliances(this is not task) and doing the following task + ``` + 1.Query weather forecast + 2.Get the current time + 3.闲聊 + ``` + You only need to classify tasks, without having to implement them personally + ## Output Format + To answer the question, Use the following JSON format. JSON only, no explanation. Otherwise, you will be punished. + The output should be formatted as a JSON instance that conforms to the format below. JSON only, no explanation. + ```json + { + "thought": "The thought of what to do and why.(use chinese)", + "action": # the action to take, must be one of provided task + { + "name": "task name,If it doesn't match, please output null", + "code": "if success output 200,If it doesn't match any task,output 400", + "answer": "Answer humanity,Be polite and gentle as much as possible,If you want to explain or add anything, it's all here.", + "control": "If user want to control the electrical output true else output false" + "value": "one of task No., json list data like [1],If it doesn't match, please output []", + } + } + ``` + """; + + public static ObjectMapper defaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); + return mapper; + } + + public void chat(String content) { + System.setProperty("org.slf4j.simpleLogger.logFile", "System.out"); + List messages = new ArrayList<>(); + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content); + ChatMessage chatMessage1 = new ChatMessage(ChatMessageRole.SYSTEM.value(), + "作为一个智能音箱,你的名字叫肖总,是由创万联团队开发的,你擅长帮助人们操作各种电器,可以联网搜索,也可以给人们很好的帮助" + + "你只可以控制下列电气:{\"lamp\",\"pump\",\"air_conditioner\"}" + + "lamp可以控制的参数:{\"switch\",\"bright\",\"color\"}" + "pump可以控制的参数:{\"switch\"}" + + "air_conditioner可以控制的参数:{\"switch\",\"temp\"}"); + // ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), + // "你能帮我查询2024年1月1日从北京南站到上海的火车票吗?"); + messages.add(chatMessage1); + messages.add(chatMessage); + String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); + // 函数调用参数构建部分 + List chatToolList = new ArrayList<>(); + ChatTool searchTool = new ChatTool(); + searchTool.setType(ChatToolType.WEB_SEARCH.value()); + WebSearch webSearch = new WebSearch(true, content); + searchTool.setWeb_search(webSearch); + + ChatTool chatTool = new ChatTool(); + chatTool.setType(ChatToolType.FUNCTION.value()); + ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters(); + chatFunctionParameters.setType("object"); + Map properties = new HashMap<>(); + properties.put("name", new HashMap() { + { + put("type", "string"); + put("description", "电气的名字"); + } + }); + properties.put("properties", new HashMap() { + { + put("type", "list"); + put("description", "电器控制的参数(英文)"); + } + }); + properties.put("value", new HashMap() { + { + put("type", "list"); + put("description", "电气控制的数值"); + } + }); + + List required = new ArrayList<>(); + required.add("name"); + required.add("properties"); + required.add("value"); + chatFunctionParameters.setProperties(properties); + ChatFunction chatFunction = + ChatFunction.builder().name("switch_electrical").description("根据用户提供的信息,打开对应的电气") + .parameters(chatFunctionParameters).required(required).build(); + chatTool.setFunction(chatFunction); + chatToolList.add(chatTool); + chatToolList.add(searchTool); + + ChatCompletionRequest chatCompletionRequest = + ChatCompletionRequest.builder().model(Constants.ModelChatGLM4).stream(Boolean.TRUE) + .messages(messages).requestId(requestId).tools(chatToolList).toolChoice("auto").build(); + ModelApiResponse sseModelApiResp = client.invokeModelApi(chatCompletionRequest); + if (sseModelApiResp.isSuccess()) { + AtomicBoolean isFirst = new AtomicBoolean(true); + ChatMessageAccumulator chatMessageAccumulator = + mapStreamToAccumulator(sseModelApiResp.getFlowable()).doOnNext(accumulator -> { + { + if (isFirst.getAndSet(false)) { + System.out.print("Response: "); + } + if (accumulator.getDelta() != null + && accumulator.getDelta().getTool_calls() != null) { + String jsonString = + mapper.writeValueAsString(accumulator.getDelta().getTool_calls()); + System.out.println("tool_calls: " + jsonString); + } + if (accumulator.getDelta() != null && accumulator.getDelta().getContent() != null) { + System.out.print(accumulator.getDelta().getContent()); + } + } + }).doOnComplete(System.out::println).lastElement().blockingGet(); + + Choice choice = new Choice(chatMessageAccumulator.getChoice().getFinishReason(), 0L, + chatMessageAccumulator.getDelta()); + List choices = new ArrayList<>(); + choices.add(choice); + ModelData data = new ModelData(); + data.setChoices(choices); + data.setUsage(chatMessageAccumulator.getUsage()); + data.setId(chatMessageAccumulator.getId()); + data.setCreated(chatMessageAccumulator.getCreated()); + data.setRequestId(chatCompletionRequest.getRequestId()); + sseModelApiResp.setFlowable(null); + sseModelApiResp.setData(data); + } + System.out.println("model output:" + JSON.toJSONString(sseModelApiResp)); + } + + public static Flowable mapStreamToAccumulator( + Flowable flowable) { + return flowable.map(chunk -> { + return new ChatMessageAccumulator(chunk.getChoices().get(0).getDelta(), null, + chunk.getChoices().get(0), chunk.getUsage(), chunk.getCreated(), chunk.getId()); + }); + } + + public JSONObject jsonChat(String content,List messages) { + + + List chatToolList = new ArrayList<>(); + ChatTool searchTool = new ChatTool(); + searchTool.setType(ChatToolType.WEB_SEARCH.value()); + WebSearch webSearch = new WebSearch(true,content); + searchTool.setWeb_search(webSearch); + chatToolList.add(searchTool); + String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); + ChatCompletionRequest chatCompletionRequest = + ChatCompletionRequest.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .tools(chatToolList) + .invokeMethod(Constants.invokeMethod).messages(messages) + .requestId(requestId).build(); + ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest); + try { + var response = mapper.writeValueAsString(invokeModelApiResp.getData().getChoices().get(0)); + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), content); + messages.add(chatMessage); + //System.out.println("model output:" + response); + var temp = response.replace("```json", "").replace("```JSON", "").replace("```", ""); + var obj= JSON.parseObject(temp); + return obj.getJSONObject("message").getJSONObject("content").getJSONObject("action"); + } catch (JsonProcessingException| JSONException e) { + var obj = new JSONObject(); + obj.put("answer","对不起拟购买的产品尚不支持这个请求,请检查你的小程序的设置"); + return obj; + } + } + + public static void main(String[] args) { + Glm glm = new Glm(); + List messages = new ArrayList<>(); + + ChatMessage chatMessage1 = new ChatMessage(ChatMessageRole.SYSTEM.value(), system_prompt2); + messages.add(chatMessage1); + Scanner scanner = new Scanner(System.in); + while (scanner.hasNextLine()) { + var content = scanner.nextLine(); + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content); + messages.add(chatMessage); + var obj=glm.jsonChat(content,messages); + if (obj.get("value")==null) { + System.out.println(obj.get("answer")); + continue; + } + System.out.println(obj.get("answer")); + // var propertyJson = obj.getJSONObject("message").getJSONObject("content") + // .getJSONObject("action").getJSONArray("properties"); + var valueJson = obj.getJSONArray("value"); + // List properties = JSONObject.parseArray(propertyJson.toJSONString(), String.class); + List value = JSONObject.parseArray(valueJson.toJSONString(), String.class); + // System.out.println(properties.toString()); + System.out.println(value.toString()); + // JsUtils.control("light1", properties, value); + } + } +} diff --git a/src/main/java/top/rslly/iot/utility/ai/IcAiException.java b/src/main/java/top/rslly/iot/utility/ai/IcAiException.java new file mode 100644 index 0000000..01ac41e --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/ai/IcAiException.java @@ -0,0 +1,11 @@ +package top.rslly.iot.utility.ai; + +import lombok.Getter; + +public class IcAiException extends Exception{ + @Getter + private final String body; + public IcAiException(String body){ + this.body = body; + } +} diff --git a/src/main/java/top/rslly/iot/utility/ai/Prompt.java b/src/main/java/top/rslly/iot/utility/ai/Prompt.java new file mode 100644 index 0000000..c02581d --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/ai/Prompt.java @@ -0,0 +1,59 @@ +package top.rslly.iot.utility.ai; + +import com.zhipu.oapi.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class Prompt { + @Autowired + private DescriptionUtil descriptionUtil; + private static final String system_prompt = + """ + As a smart speaker, your name is {agent_name}, developed by the {team_name} team. You are good at helping people operate various appliances, + You can only control the following electrical:{electrical_name} + The electrical properties and value:{properties_value} + ## Output Format + To answer the question, Use the following JSON format. JSON only, no explanation. Otherwise, you will be punished. + The output should be formatted as a JSON instance that conforms to the format below. JSON only, no explanation. + + ```json + { + "thought": "The thought of what to do and why.(use chinese)", + "action": # the action to take, must be one of provided tools + { + "name": "electronic name,If it doesn't match, please output null", + "code": "if success output 200,else output 400", + "answer": "Answer humanity,Be polite and gentle as much as possible", + "properties": "electronic input parameters, json list data,If there is no such properties, please output []", + "value": "electronic input parameters, json list data like ["on",30],If it doesn't match, please output []", + } + } + ``` + If you find that the appliance and properties that the user wants to control is not in the above range, please return it in the following format + ```json + { + "thought": "The thought of what to do and why.", + "action": # the action to take, must be one of provided tools + { + "name": "null", + "code": "400", + "answer": "Answer humanity,Be polite and gentle as much as possible", + "properties": [], + "value": [], + } + } + ``` + """; + public String GetControlTool(){ + Map params = new HashMap<>(); + params.put("agent_name", "小优"); + params.put("team_name","创万联"); + params.put("electrical_name", descriptionUtil.GetElectricalName()); + params.put("properties_value", descriptionUtil.GetPropertiesAndValue()); + return StringUtils.formatString(system_prompt, params); + } +} diff --git a/src/main/java/top/rslly/iot/utility/ai/tools/ControlTool.java b/src/main/java/top/rslly/iot/utility/ai/tools/ControlTool.java new file mode 100644 index 0000000..3210ca8 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/ai/tools/ControlTool.java @@ -0,0 +1,76 @@ +package top.rslly.iot.utility.ai.tools; + +import com.alibaba.fastjson.JSONObject; +import com.zhipu.oapi.service.v4.model.ChatMessage; +import com.zhipu.oapi.service.v4.model.ChatMessageRole; +import lombok.Data; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import top.rslly.iot.param.request.ControlParam; +import top.rslly.iot.services.HardWareServiceImpl; +import top.rslly.iot.services.ProductDeviceServiceImpl; +import top.rslly.iot.services.ProductModelServiceImpl; +import top.rslly.iot.utility.ai.DescriptionUtil; +import top.rslly.iot.utility.ai.Glm; +import top.rslly.iot.utility.ai.IcAiException; +import top.rslly.iot.utility.ai.Prompt; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Component +public class ControlTool { + @Autowired + private Prompt prompt; + @Autowired + private ProductDeviceServiceImpl productDeviceService; + @Autowired + private ProductModelServiceImpl productModelService; + @Autowired + private HardWareServiceImpl hardWareService; + + private String name = "ControlTool"; + private String description = """ + An IoT tool used for switching operations on various devices. + This tool uses the mqtt protocol for transmission. + Args: question(str) + """; + public String run(String question){ + Glm glm = new Glm(); + List messages = new ArrayList<>(); + + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), prompt.GetControlTool()); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), question); + messages.add(systemMessage); + messages.add(userMessage); + var obj=glm.jsonChat(question,messages); + var answer = (String) obj.get("answer"); + try { + this.process_llm_result(obj); + } catch (Exception e) { + return answer; + } + return answer; + } + private void process_llm_result(JSONObject jsonObject) throws MqttException, IcAiException { + if(jsonObject.get("code").equals("200")){ + var propertyJson = jsonObject.getJSONArray("properties"); + var valueJson = jsonObject.getJSONArray("value"); + List properties = JSONObject.parseArray(propertyJson.toJSONString(), String.class); + List value = JSONObject.parseArray(valueJson.toJSONString(), String.class); + var productModels=productModelService.findAllByName(jsonObject.get("name").toString()); + if (productModels.isEmpty()) throw new IcAiException("platform not support"); + for(var s:productModels){ + var deviceNames = productDeviceService.findAllByModelId(s.getId()); + if (deviceNames.isEmpty()) throw new IcAiException("platform not support"); + for (var device:deviceNames) { + ControlParam controlParam = new ControlParam(device.getName(), properties, value); + var res=hardWareService.control(controlParam); + if(res.getErrorCode()!=200)throw new IcAiException("platform not support"); + } + } + } + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBBaseMapper.java b/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBBaseMapper.java new file mode 100644 index 0000000..2bc19be --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBBaseMapper.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import org.springframework.data.repository.NoRepositoryBean; +import top.rslly.iot.utility.influxdb.ano.Insert; + +import java.util.List; + +public interface InfluxDBBaseMapper { + @Insert + void insert(T object); +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBConfig.java b/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBConfig.java new file mode 100644 index 0000000..429009a --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/InfluxDBConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.ResourcePatternResolver; + +@Configuration +public class InfluxDBConfig implements EnvironmentAware { + private Environment env; + + @Bean(name = "proxyMapperRegister") + public ProxyMapperRegister proxyMapperRegister(ResourcePatternResolver resourcePatternResolver, + ApplicationContext applicationContext) { + String mapperLocation = env.getProperty("spring.influx.mapper-location"); + return new ProxyMapperRegister(resourcePatternResolver, applicationContext, mapperLocation); + } + + @Override + public void setEnvironment(Environment environment) { + this.env = environment; + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/InfluxProperty.java b/src/main/java/top/rslly/iot/utility/influxdb/InfluxProperty.java new file mode 100644 index 0000000..7667bf6 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/InfluxProperty.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "spring.influx") +public class InfluxProperty { + /** + * 数据库名 + */ + private String dataBaseName; + private String url; + private String user; + private String password; + private String retention; +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/InfluxProxyMapperFactory.java b/src/main/java/top/rslly/iot/utility/influxdb/InfluxProxyMapperFactory.java new file mode 100644 index 0000000..d53aa6d --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/InfluxProxyMapperFactory.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import top.rslly.iot.utility.influxdb.executor.Executor; + +import java.lang.reflect.Proxy; + +public class InfluxProxyMapperFactory implements FactoryBean { + + @Autowired + Executor executor; + + private Class interfaceClass; + + public InfluxProxyMapperFactory(Class interfaceClass) { + this.interfaceClass = interfaceClass; + } + + + @Override + public Object getObject() throws Exception { + Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), + new Class[] {interfaceClass}, new ProxyMapper(new ParameterHandler(), executor)); + return proxyInstance; + } + + @Override + public Class getObjectType() { + return interfaceClass; + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ParameterHandler.java b/src/main/java/top/rslly/iot/utility/influxdb/ParameterHandler.java new file mode 100644 index 0000000..b4f052c --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ParameterHandler.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import top.rslly.iot.utility.influxdb.ano.Param; + +import java.lang.reflect.Parameter; + +public class ParameterHandler { + /** + * 拼接sql + * + * @param parameters 参数名 + * @param args 参数实际值 + * @param sql 未拼接参数的sql语句 + * @return 拼接好的sql + */ + public String handleParameter(Parameter[] parameters, Object[] args, String sql) { + for (int i = 0; i < parameters.length; i++) { + Class parameterType = parameters[i].getType(); + String parameterName = parameters[i].getName(); + + Param param = parameters[i].getAnnotation(Param.class); + if (param != null) { + parameterName = param.value(); + } + + if (parameterType == String.class) { + sql = sql.replaceAll("\\#\\{" + parameterName + "\\}", "'" + args[i] + "'"); + } else { + sql = sql.replaceAll("\\#\\{" + parameterName + "\\}", args[i].toString()); + } + sql = sql.replaceAll("\\$\\{" + parameterName + "\\}", args[i].toString()); + } + return sql; + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapper.java b/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapper.java new file mode 100644 index 0000000..79dfe8d --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapper.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + *

+ * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import lombok.extern.slf4j.Slf4j; +import top.rslly.iot.utility.influxdb.ano.Delete; +import top.rslly.iot.utility.influxdb.ano.Insert; +import top.rslly.iot.utility.influxdb.ano.Select; +import top.rslly.iot.utility.influxdb.executor.Executor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; + +@Slf4j +public class ProxyMapper implements InvocationHandler { + private ParameterHandler parameterHandler; + private Executor executor; + + public ProxyMapper(ParameterHandler parameterHandler, Executor executor) { + this.parameterHandler = parameterHandler; + this.executor = executor; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + Annotation[] annotations = method.getAnnotations(); + + if (annotations.length == 1) { + Annotation annotation = annotations[0]; + Class annotationType = annotation.annotationType(); + if (annotationType == Select.class) { + Select selectAnnotation = method.getAnnotation(Select.class); + // 拼接sql + String sql = selectAnnotation.value(); + Parameter[] parameters = method.getParameters(); + sql = parameterHandler.handleParameter(parameters, args, sql); + // log.info(sql); + // 查询结果 + Class resultType = selectAnnotation.resultType(); + return executor.query(resultType, sql); + } else if (annotationType == Insert.class) { + executor.insert(args); + } else if (annotationType == Delete.class) { + Delete deleteAnnotation = method.getAnnotation(Delete.class); + //拼接sql + String sql = deleteAnnotation.value(); + Parameter[] parameters = method.getParameters(); + + sql = parameterHandler.handleParameter(parameters, args, sql); + //执行sql + executor.delete(sql); + } + } + return null; + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapperRegister.java b/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapperRegister.java new file mode 100644 index 0000000..1a7780d --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ProxyMapperRegister.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.ClassUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class ProxyMapperRegister + implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware, ApplicationContextAware { + private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + private MetadataReaderFactory metadataReaderFactory; + + private ResourcePatternResolver resourcePatternResolver; + + private ApplicationContext applicationContext; + + private String mapperLocation; + + public ProxyMapperRegister(ResourcePatternResolver resourcePatternResolver, + ApplicationContext applicationContext, String mapperLocation) { + this.resourcePatternResolver = resourcePatternResolver; + this.applicationContext = applicationContext; + this.mapperLocation = mapperLocation; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) + throws BeansException { + // 获取启动类所在包 + List packages = new ArrayList<>(); + packages.add(mapperLocation); + + // 开始扫描包,获取字节码 + Set> beanClazzSet = scannerPackages(packages.get(0)); + for (Class beanClazz : beanClazzSet) { + // 判断是否是需要被代理的接口 + // BeanDefinition构建器 + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz); + GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition(); + + // 在这里,我们可以给该对象的属性注入对应的实例。 + definition.getConstructorArgumentValues().addGenericArgumentValue(beanClazz); + + // 定义Bean工程(最终会用上面add的构造函数参数值作为参数调用RepositoryFactory的构造方法) + definition.setBeanClass(InfluxProxyMapperFactory.class); + // 这里采用的是byType方式注入,类似的还有byName等 + definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE); + String simpleName = beanClazz.getSimpleName(); + // 首字母小写注入容器 + simpleName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1); + beanDefinitionRegistry.registerBeanDefinition(simpleName, definition); + } + } + + @Override + public void postProcessBeanFactory( + ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { + + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + } + + private Boolean needProxy(Class beanClazz) { + Class[] interfaces = beanClazz.getInterfaces(); + for (Class anInterface : interfaces) { + if (anInterface == InfluxDBBaseMapper.class) { + return true; + } + } + return false; + } + + private Set> scannerPackages(String basePackage) { + Set> set = new LinkedHashSet<>(); + // 此处固定写法即可,含义就是包及子包下的所有类 + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + resolveBasePackage(basePackage) + '/' + DEFAULT_RESOURCE_PATTERN; + try { + Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource); + String className = metadataReader.getClassMetadata().getClassName(); + Class clazz; + try { + clazz = Class.forName(className); + if (needProxy(clazz)) { + set.add(clazz); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return set; + } + + private String resolveBasePackage(String basePackage) { + // 将类名转换为资源路径 + return ClassUtils.convertClassNameToResourcePath( + // 解析占位符 + this.applicationContext.getEnvironment().resolveRequiredPlaceholders(basePackage)); + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ReflectUtils.java b/src/main/java/top/rslly/iot/utility/influxdb/ReflectUtils.java new file mode 100644 index 0000000..d1dc68b --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ReflectUtils.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb; + +import org.nutz.lang.Lang; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +public class ReflectUtils { + public static String getField(Object object, Class annotation) { + Object first = Lang.first(object); + Class clazz = first.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + boolean isAnon = field.isAnnotationPresent(annotation); + if (isAnon) { + return field.getName(); + } + } + return null; + } + + public static List getFields(Object object, Class annotation) { + Object first = Lang.first(object); + Class clazz = first.getClass(); + Field[] fields = clazz.getDeclaredFields(); + List fieldsName = new ArrayList<>(); + for (Field field : fields) { + field.setAccessible(true); + boolean isAnon = field.isAnnotationPresent(annotation); + if (isAnon) { + fieldsName.add(field.getName()); + } + } + return fieldsName; + } +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/Delete.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/Delete.java new file mode 100644 index 0000000..ef6fc62 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/Delete.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Delete { + String value(); +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/Insert.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/Insert.java new file mode 100644 index 0000000..89a3c01 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/Insert.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Insert { +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/Param.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/Param.java new file mode 100644 index 0000000..e73430b --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/Param.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +@Documented +public @interface Param { + String value(); +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/Select.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/Select.java new file mode 100644 index 0000000..073df24 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/Select.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Select { + String value(); + + Class resultType(); +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/Tag.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/Tag.java new file mode 100644 index 0000000..35d8389 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/Tag.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Tag { +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/ano/TimeColumn.java b/src/main/java/top/rslly/iot/utility/influxdb/ano/TimeColumn.java new file mode 100644 index 0000000..af221e1 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/ano/TimeColumn.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TimeColumn { +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/executor/Executor.java b/src/main/java/top/rslly/iot/utility/influxdb/executor/Executor.java new file mode 100644 index 0000000..460c928 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/executor/Executor.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.executor; + +import java.util.List; + +public interface Executor { + /** + * 测试连接是否正常 + * + * @return + */ + Boolean ping(); + + /** + * 创建数据库 说明:方法参数没有指定时,默认使用配置文件中数据库名 + */ + void createDataBase(String... dataBaseName); + + /** + * 删除数据库 说明:方法参数没有指定时,默认使用配置文件中数据库名 + */ + void deleteDataBase(String... dataBaseName); + + /** + * 插入数据 支持:对象,集合(集合时对应实体类必须使用@Tag注解指定一个字段) + * + * @param object 数据 + */ + void insert(T object); + + /** + * 查询数据 + * + * @param sql string + */ + List query(Class clazz, String sql); + /** + * delete data + * @param sql string + */ + void delete(String sql); +} diff --git a/src/main/java/top/rslly/iot/utility/influxdb/executor/ExecutorImpl.java b/src/main/java/top/rslly/iot/utility/influxdb/executor/ExecutorImpl.java new file mode 100644 index 0000000..1e73be6 --- /dev/null +++ b/src/main/java/top/rslly/iot/utility/influxdb/executor/ExecutorImpl.java @@ -0,0 +1,271 @@ +/** + * Copyright © 2023-2030 The ruanrongman Authors + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package top.rslly.iot.utility.influxdb.executor; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDB.ConsistencyLevel; +import org.influxdb.InfluxDBFactory; +import org.influxdb.annotation.Measurement; +import org.influxdb.dto.BatchPoints; +import org.influxdb.dto.Point; +import org.influxdb.dto.Point.Builder; +import org.influxdb.dto.Pong; +import org.influxdb.dto.Query; +import org.influxdb.dto.QueryResult; +import org.influxdb.dto.QueryResult.Result; +import org.influxdb.dto.QueryResult.Series; +import org.nutz.lang.Lang; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import top.rslly.iot.utility.influxdb.InfluxProperty; +import top.rslly.iot.utility.influxdb.ReflectUtils; +import top.rslly.iot.utility.influxdb.ano.Tag; +import top.rslly.iot.utility.influxdb.ano.TimeColumn; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.time.Instant; +import java.time.temporal.ChronoField; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class ExecutorImpl implements Executor { + + private InfluxDB influxDB; + + @Autowired + private InfluxProperty influxProperty; + + public static long convertToUnixTimestamp(String timestamp) { + Instant instant = Instant.parse(timestamp); + long seconds = instant.getEpochSecond(); + int milliseconds = instant.get(ChronoField.MILLI_OF_SECOND); + return seconds * 1000 + milliseconds; + } + + @Bean + @SuppressWarnings("deprecation") + public InfluxDB influxDbBuild() { + if (influxDB == null) { + influxDB = InfluxDBFactory.connect(influxProperty.getUrl(), influxProperty.getUser(), + influxProperty.getPassword()); + } + try { + if (!influxDB.databaseExists(influxProperty.getDataBaseName())) { + influxDB.createDatabase(influxProperty.getDataBaseName()); + } + } catch (Exception e) { + // 该数据库可能设置动态代理,不支持创建数据库 + e.printStackTrace(); + } finally { + influxDB.setRetentionPolicy(influxProperty.getRetention()); + } + influxDB.setLogLevel(InfluxDB.LogLevel.NONE); + return influxDB; + } + + @Override + public Boolean ping() { + boolean isConnected = false; + Pong pong; + try { + pong = influxDB.ping(); + if (pong != null) { + isConnected = true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return isConnected; + } + + @Override + @SuppressWarnings("deprecation") + public void createDataBase(String... dataBaseName) { + if (dataBaseName.length > 0) { + influxDB.createDatabase(dataBaseName[0]); + return; + } + if (influxProperty.getDataBaseName() == null) { + log.error("如参数不指定数据库名,配置文件 spring.influx.dataBaseName 必须指定"); + return; + } + influxDB.createDatabase(influxProperty.getDataBaseName()); + } + + @Override + @SuppressWarnings("deprecation") + public void deleteDataBase(String... dataBaseName) { + if (dataBaseName.length > 0) { + influxDB.deleteDatabase(dataBaseName[0]); + return; + } + if (influxProperty.getDataBaseName() == null) { + log.error("如参数不指定数据库名,配置文件 spring.influx.dataBaseName 必须指定"); + return; + } + influxDB.deleteDatabase(influxProperty.getDataBaseName()); + } + + @Override + public void insert(T object) { + // 构建一个Entity + Object first = Lang.first(object); + Class clazz = first.getClass(); + // 表名 + boolean isAnnot = clazz.isAnnotationPresent(Measurement.class); + if (!isAnnot) { + log.error("插入的数据对应实体类需要@Measurement注解"); + return; + } + Measurement annotation = clazz.getAnnotation(Measurement.class); + // 表名 + String measurement = annotation.name(); + Field[] arrfield = clazz.getDeclaredFields(); + // 数据长度 + int size = Lang.eleSize(object); + List tagFields = ReflectUtils.getFields(object, Tag.class); + String timeField = ReflectUtils.getField(object, TimeColumn.class); + if (tagFields.isEmpty()) { + log.error("插入多条数据需对应实体类字段有@Tag注解"); + return; + } + BatchPoints batchPoints = BatchPoints.database(influxProperty.getDataBaseName()) + // 一致性 + .consistency(ConsistencyLevel.ALL).build(); + for (int i = 0; i < size; i++) { + Map map = new HashMap<>(); + Builder builder = Point.measurement(measurement); + for (Field field : arrfield) { + // 私有属性需要开启 + field.setAccessible(true); + Object result = first; + try { + if (size > 1) { + List objects = (List) (object); + result = objects.get(i); + } + if (tagFields.contains(field.getName())) { + builder.tag(field.getName(), field.get(result).toString()); + } else if (field.getName().equals(timeField)) { + builder.time(Long.parseLong(field.get(result).toString()), annotation.timeUnit()); + } else { + map.put(field.getName(), field.get(result)); + } + } catch (IllegalAccessException e) { + log.error("实体转换出错"); + e.printStackTrace(); + } + } + builder.fields(map); + batchPoints.point(builder.build()); + } + influxDB.write(batchPoints); + } + + @Override + public List query(Class clazz, String sql) { + if (influxProperty.getDataBaseName() == null) { + log.error("查询数据时配置文件 spring.influx.dataBaseName 必须指定"); + return null; + } + QueryResult results = influxDB.query(new Query(sql, influxProperty.getDataBaseName())); + if (results != null) { + if (results.getResults() == null) { + return null; + } + List list = new ArrayList<>(); + + for (Result result : results.getResults()) { + List series = result.getSeries(); + if (series == null) { + // list.add(null); + continue; + } + for (Series serie : series) { + List> values = serie.getValues(); + List columns = serie.getColumns(); + // 构建Bean + list.addAll(getQueryData(clazz, columns, values)); + } + } + log.info(list.toString()); + return JSON.parseArray(JSON.toJSONString(list), clazz); + } + return null; + } + + @Override + public void delete(String sql) { + influxDB.query(new Query(sql, influxProperty.getDataBaseName())); + } + + /** + * 自动转换对应Pojo + * + * @param values + * @return + */ + public List getQueryData(Class clazz, List columns, List> values) { + List results = new ArrayList<>(); + for (List list : values) { + BeanWrapperImpl bean = null; + Object result = null; + try { + result = clazz.getDeclaredConstructor().newInstance(); + bean = new BeanWrapperImpl(result); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException + | InvocationTargetException e) { + e.printStackTrace(); + } + String timeField = ReflectUtils.getField(result, TimeColumn.class); + for (int i = 0; i < list.size(); i++) { + // 字段名 + String filedName = columns.get(i); + if (filedName.equals("Tag")) { + continue; + } + try { + Field field = clazz.getDeclaredField(filedName); + } catch (NoSuchFieldException e) { + continue; + } + // 值 + Object value = list.get(i); + if (filedName.equals(timeField)) { + value = convertToUnixTimestamp((String) value); + } + if (bean != null) { + bean.setPropertyValue(filedName, value); + } + } + results.add(result); + } + return results; + } +} diff --git a/src/main/java/top/rslly/iot/utility/script/js/AbstractJsInvokeService.java b/src/main/java/top/rslly/iot/utility/script/js/AbstractJsInvokeService.java index 9ddb201..d6fb6ad 100644 --- a/src/main/java/top/rslly/iot/utility/script/js/AbstractJsInvokeService.java +++ b/src/main/java/top/rslly/iot/utility/script/js/AbstractJsInvokeService.java @@ -28,4 +28,9 @@ public abstract class AbstractJsInvokeService { protected abstract ListenableFuture doEval(UUID scriptId, JsScriptInfo scriptInfo, String jsScript); + protected abstract ListenableFuture doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, + Object[] args); + + protected abstract void doRelease(UUID scriptId, JsScriptInfo scriptInfo) throws Exception; + } diff --git a/src/main/java/top/rslly/iot/utility/script/js/NashornJsInvokeService.java b/src/main/java/top/rslly/iot/utility/script/js/NashornJsInvokeService.java index b3b7b0e..7e1ad78 100644 --- a/src/main/java/top/rslly/iot/utility/script/js/NashornJsInvokeService.java +++ b/src/main/java/top/rslly/iot/utility/script/js/NashornJsInvokeService.java @@ -26,11 +26,16 @@ import delight.nashornsandbox.NashornSandboxes; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import top.rslly.iot.utility.script.*; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; +import javax.script.ScriptException; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -38,7 +43,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantLock; -// @Service +@Service public class NashornJsInvokeService extends AbstractJsInvokeService implements ScriptInvokeService { protected final Map scriptInfoMap = new ConcurrentHashMap<>(); private NashornSandbox sandbox; @@ -57,10 +62,6 @@ public class NashornJsInvokeService extends AbstractJsInvokeService implements S @Value("${js.local.max_cpu_time}") private long maxCpuTime; - @Getter - @Value("${js.local.max_errors}") - private int maxErrors; - @Getter @Value("${js.local.max_black_list_duration_sec:60}") private int maxBlackListDurationSec; @@ -69,13 +70,10 @@ public class NashornJsInvokeService extends AbstractJsInvokeService implements S @Value("${js.local.max_requests_timeout:0}") private long maxInvokeRequestsTimeout; - @Getter - @Value("${js.local.stats.enabled:false}") - private boolean statsEnabled; - @Value("${js.local.js_thread_pool_size:50}") private int jsExecutorThreadPoolSize; + @PostConstruct @Override public void init() { jsExecutor = @@ -87,6 +85,7 @@ public void init() { sandbox.setExecutor(monitorExecutorService); sandbox.setMaxCPUTime(maxCpuTime); sandbox.allowNoBraces(false); + sandbox.allow(JsUtils.class); sandbox.allowLoadFunctions(true); sandbox.setMaxPreparedStatements(30); } else { @@ -115,6 +114,7 @@ public ScriptLanguage getLanguage() { return ScriptLanguage.JS; } + @PreDestroy @Override public void stop() { if (monitorExecutorService != null) { @@ -146,4 +146,31 @@ protected ListenableFuture doEval(UUID scriptId, JsScriptInfo scriptInfo, } }); } + + @Override + protected ListenableFuture doInvokeFunction(UUID scriptId, JsScriptInfo scriptInfo, + Object[] args) { + return jsExecutor.submit(() -> { + try { + if (useJsSandbox) { + return sandbox.getSandboxedInvocable().invokeFunction(scriptInfo.getFunctionName(), args); + } else { + return ((Invocable) engine).invokeFunction(scriptInfo.getFunctionName(), args); + } + } catch (ScriptException e) { + throw new IcScriptException(scriptId, IcScriptException.ErrorCode.RUNTIME, null, e); + } catch (Exception e) { + throw new IcScriptException(scriptId, IcScriptException.ErrorCode.OTHER, null, e); + } + }); + } + + @Override + protected void doRelease(UUID scriptId, JsScriptInfo scriptInfo) throws Exception { + if (useJsSandbox) { + sandbox.eval(scriptInfo.getFunctionName() + " = undefined;"); + } else { + engine.eval(scriptInfo.getFunctionName() + " = undefined;"); + } + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3c888ab..f99c47e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,11 +3,20 @@ spring: pathmatch: matching-strategy: ant_path_matcher jpa: - show-sql: true + show-sql: false + influx: + user: cwl + # 你的数据库url + url: http://192.168.49.129:8086 + password: 1234 + mapper-location: top.rslly.iot.dao + dataBaseName: data + retention: autogen #保存策略 datasource: username: cwliot1.8 url: jdbc:mysql://192.168.49.129:3306/cwliot1.8?useUnicode=true&characterEncoding=UTF-8&useSSL=false driver-class-name: com.mysql.jdbc.Driver + # 输入你自己的mysql密码,该密码为随机生成 password: "DMKRjBbHCiPAyAdn" type: com.alibaba.druid.pool.DruidDataSource druid: @@ -29,7 +38,7 @@ spring: # 打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 - # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙 日志 log4j + # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙 日志 log4j # filters: stat,wall,log4j #导入了log4j use-global-data-source-stat: true # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 @@ -50,13 +59,20 @@ grpc: port: 9090 mqtt: Host: tcp://192.168.49.129:1883 - username: cwl - password: 19260817 + username: hello + password: 1234 wx: debug: true micro: appid: 123 - appsecret : 123 + appsecret: 123 +storage: + database: influxdb js: local: - use_js_sandbox: true \ No newline at end of file + use_js_sandbox: true + monitor_thread_pool_size: 4 + max_cpu_time: 300000 + max_black_list_duration_sec: 60 + maxInvokeRequestsTimeout: 0 + js_thread_pool_size: 50 diff --git a/src/test/java/top/rslly/iot/DemoApplicationTests.java b/src/test/java/top/rslly/iot/DemoApplicationTests.java index 27e0e78..7efa549 100644 --- a/src/test/java/top/rslly/iot/DemoApplicationTests.java +++ b/src/test/java/top/rslly/iot/DemoApplicationTests.java @@ -20,20 +20,39 @@ package top.rslly.iot; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import top.rslly.iot.dao.TimeDataRepository; +import top.rslly.iot.models.DataEntity; +import top.rslly.iot.models.influxdb.DataTimeEntity; import top.rslly.iot.param.request.ControlParam; import top.rslly.iot.utility.DataCleanAuto; import top.rslly.iot.utility.DataSave; +import top.rslly.iot.utility.ai.tools.ControlTool; +import top.rslly.iot.utility.influxdb.executor.ExecutorImpl; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; -// @SpringBootTest() +@RunWith(SpringRunner.class) +@SpringBootTest() +@Slf4j class DemoApplicationTests { + @Autowired + private DataCleanAuto dataCleanAuto; + @Autowired + private ExecutorImpl dataTimeRepository; + @Autowired(required = false) + private TimeDataRepository timeDataRepository; + @Autowired + private ControlTool controlTool; @Test void testDataSave() { @@ -53,8 +72,77 @@ void testJsUtil() { Assertions.assertEquals("100", controlParam.getValue().get(0)); } - /* - * @Test void testDataAutoClean() throws IOException { dataCleanAuto.task(); } + + @Test + void testDataAutoClean() throws IOException { + dataCleanAuto.task(); + } + + @Test + public void ping() { + if (dataTimeRepository.ping()) { + log.info("连接成功"); + } else { + log.info("连接失败"); + } + } + + @Test + void insert() { + DataTimeEntity dataTimeEntity = new DataTimeEntity(); + long time = System.currentTimeMillis(); + dataTimeEntity.setTime(time); + System.out.println(time); + dataTimeEntity.setDeviceId(1); + dataTimeEntity.setJsonKey("temp"); + dataTimeEntity.setValue("30"); + dataTimeEntity.setCharacteristic(UUID.randomUUID().toString()); + dataTimeRepository.insert(dataTimeEntity); + } + + @Test + public void list() { + String sql = "SELECT * FROM \"data\" tz('Asia/Shanghai')"; + List locations = dataTimeRepository.query(DataTimeEntity.class, sql); + if (locations == null || locations.isEmpty()) { + log.info("暂无数据"); + } + System.out.println(locations.get(0).getTime()); + } + + /** + * 删除数据库 */ + @Test + public void delete() { + // 默认使用配置文件中数据库 + // influxDao.deleteDataBase(); + // 使用指定数据库 + dataTimeRepository.deleteDataBase("data"); + } + + @Test + public void insert2() { + DataEntity dataTimeEntity = new DataEntity(); + long time = System.currentTimeMillis(); + dataTimeEntity.setTime(time); + System.out.println(time); + dataTimeEntity.setDeviceId(2); + dataTimeEntity.setJsonKey("temp"); + dataTimeEntity.setValue("90"); + dataTimeEntity.setCharacteristic(UUID.randomUUID().toString()); + timeDataRepository.insert(dataTimeEntity); + } + + @Test + public void time() { + var res = timeDataRepository.findAllByTimeBetweenAndDeviceId(0L, System.currentTimeMillis(), 1); + System.out.println(res.get(0)); + } + @Test + public void Ai(){ + var answer=controlTool.run("太暗了"); + System.out.println(answer); + } }