From 4196e0554b15d5f274af239017699b2f22e0b420 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 18 Jul 2024 20:07:25 +0800 Subject: [PATCH 01/90] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0docker-compose?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker-compose.yml | 393 ++++++++++++++++++++++++++++++++ docker/iot/docker-compose.yml | 13 ++ docker/mysql/docker-compose.yml | 18 ++ 3 files changed, 424 insertions(+) create mode 100644 docker/docker-compose.yml create mode 100644 docker/iot/docker-compose.yml create mode 100644 docker/mysql/docker-compose.yml diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..fa92010 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,393 @@ +version: '3' + +services: + cassandra: + image: cassandra:latest + container_name: cassandra-container + ports: + - "9042:9042" + environment: + - CASSANDRA_USER=admin + - CASSANDRA_PASSWORD=admin + volumes: + - Cassandra/dataa:/var/lib/cassandra + networks: + - iot-net + clickhouse: + image: yandex/clickhouse-server:22.1.3.7 + container_name: clickhouse + restart: always + ports: + - "8123:8123" + - "9000:9000" + volumes: + # 默认配置 + - clickhouse/config/docker_related_config.xml:/etc/clickhouse-server/config.d/docker_related_config.xml:rw + - clickhouse/config/config.xml:/etc/clickhouse-server/config.xml:rw + - clickhouse/config/users.xml:/etc/clickhouse-server/users.xml:rw + - /etc/localtime:/etc/localtime:ro + # 运行日志 + - clickhouse/log:/var/log/clickhouse-server + # 数据持久 + - clickhouse/data:/var/lib/clickhouse:rw + networks: + - iot-net + influxdb: + image: influxdb:2.6-alpine + env_file: + - influxv2.env + volumes: + # Mount for influxdb data directory and configuration + - influxdbv2:/var/lib/influxdb2:rw + ports: + - "8086:8086" + networks: + - iot-net + telegraf: + image: telegraf:1.25-alpine + depends_on: + - influxdb + volumes: + # Mount for telegraf config + - influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro + env_file: + - influx/influxv2.env + networks: + - iot-net + iotdb-service: + image: apache/iotdb:1.3.0-standalone + hostname: iotdb-service + container_name: iotdb-service + ports: + - "6667:6667" + environment: + - cn_internal_address=iotdb-service + - cn_internal_port=10710 + - cn_consensus_port=10720 + - cn_seed_config_node=iotdb-service:10710 + - dn_rpc_address=iotdb-service + - dn_internal_address=iotdb-service + - dn_rpc_port=6667 + - dn_mpp_data_exchange_port=10740 + - dn_schema_region_consensus_port=10750 + - dn_data_region_consensus_port=10760 + - dn_seed_config_node=iotdb-service:10710 + volumes: + - ./data:/iotdb/data + - ./logs:/iotdb/logs + networks: + - iot-net +# zookeeper: +# image: wurstmeister/zookeeper +# container_name: zookeeper +# ports: +# - "2181:2181" + kafka: + image: wurstmeister/kafka + container_name: kafka + volumes: + - /etc/localtime:/etc/localtime + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_PORT: 9092 + KAFKA_LOG_RETENTION_HOURS: 120 + KAFKA_MESSAGE_MAX_BYTES: 10000000 + KAFKA_REPLICA_FETCH_MAX_BYTES: 10000000 + KAFKA_GROUP_MAX_SESSION_TIMEOUT_MS: 60000 + KAFKA_NUM_PARTITIONS: 3 + KAFKA_DELETE_RETENTION_MS: 1000 + networks: + - iot-net + kafka-manager: + image: sheepkiller/kafka-manager + container_name: kafka-manager + environment: + ZK_HOSTS: 127.0.0.1 + ports: + - "9009:9000" + networks: + - iot-net + mongodb: + image: mongo + container_name: mongodb + ports: + - 27017:27017 + volumes: + - ./database:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + networks: + - iot-net + mongo-express: + image: mongo-express + container_name: mongo-express + restart: always + ports: + - 8181:8081 + environment: + - ME_CONFIG_MONGODB_ADMINUSERNAME=admin + - ME_CONFIG_MONGODB_ADMINPASSWORD=admin + - ME_CONFIG_MONGODB_SERVER=mongodb + networks: + - iot-net + emqx1: + image: emqx:5.4.1 + container_name: emqx1 + environment: + - "EMQX_NODE_NAME=emqx@node1.emqx.io" + - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" + - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" + healthcheck: + test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] + interval: 5s + timeout: 25s + retries: 5 + networks: + iot-net: + aliases: + - node1.emqx.io + ports: + - 1883:1883 + - 8083:8083 + - 8084:8084 + - 8883:8883 + - 18083:18083 + # volumes: + # - $PWD/emqx1_data:/opt/emqx/data + + emqx2: + image: emqx:5.4.1 + container_name: emqx2 + environment: + - "EMQX_NODE_NAME=emqx@node2.emqx.io" + - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" + - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" + healthcheck: + test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] + interval: 5s + timeout: 25s + retries: 5 + networks: + iot-net: + aliases: + - node2.emqx.io + mysql: + image: mysql:8.0 + container_name: mysql8 + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: mysql + MYSQL_USER: iot + MYSQL_PASSWORD: iot123456 + TZ: "Asia/Shanghai" + volumes: + - mysql/data:/var/lib/mysql + ports: + - "3306:3306" + command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --binlog-format=ROW + networks: + - iot-net + + # Start zookeeper + zookeeper: + image: apachepulsar/pulsar:latest + container_name: zookeeper + restart: on-failure + networks: + - iot-net + volumes: + - Pulsar/data/zookeeper:/pulsar/data/zookeeper + environment: + - metadataStoreUrl=zk:zookeeper:2181 + - PULSAR_MEM=-Xms256m -Xmx256m -XX:MaxDirectMemorySize=256m + command: > + bash -c "bin/apply-config-from-env.py conf/zookeeper.conf && \ + bin/generate-zookeeper-config.sh conf/zookeeper.conf && \ + exec bin/pulsar zookeeper" + healthcheck: + test: ["CMD", "bin/pulsar-zookeeper-ruok.sh"] + interval: 10s + timeout: 5s + retries: 30 + + # Init cluster metadata + pulsar-init: + container_name: pulsar-init + hostname: pulsar-init + image: apachepulsar/pulsar:latest + networks: + - iot-net + command: > + bin/pulsar initialize-cluster-metadata \ + --cluster cluster-a \ + --zookeeper zookeeper:2181 \ + --configuration-store zookeeper:2181 \ + --web-service-url http://broker:8080 \ + --broker-service-url pulsar://broker:6650 + depends_on: + zookeeper: + condition: service_healthy + + # Start bookie + bookie: + image: apachepulsar/pulsar:latest + container_name: bookie + restart: on-failure + networks: + - iot-net + environment: + - clusterName=cluster-a + - zkServers=zookeeper:2181 + - metadataServiceUri=metadata-store:zk:zookeeper:2181 + # otherwise every time we run docker compose uo or down we fail to start due to Cookie + # See: https://github.com/apache/bookkeeper/blob/405e72acf42bb1104296447ea8840d805094c787/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java#L57-68 + - advertisedAddress=bookie + - BOOKIE_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m + depends_on: + zookeeper: + condition: service_healthy + pulsar-init: + condition: service_completed_successfully + # Map the local directory to the container to avoid bookie startup failure due to insufficient container disks. + volumes: + - Pulsar/data/bookkeeper:/pulsar/data/bookkeeper + command: bash -c "bin/apply-config-from-env.py conf/bookkeeper.conf && exec bin/pulsar bookie" + + # Start broker + broker: + image: apachepulsar/pulsar:latest + container_name: broker + hostname: broker + restart: on-failure + networks: + - iot-net + environment: + - metadataStoreUrl=zk:zookeeper:2181 + - zookeeperServers=zookeeper:2181 + - clusterName=cluster-a + - managedLedgerDefaultEnsembleSize=1 + - managedLedgerDefaultWriteQuorum=1 + - managedLedgerDefaultAckQuorum=1 + - advertisedAddress=broker + - advertisedListeners=external:pulsar://127.0.0.1:6650 + - PULSAR_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m + depends_on: + zookeeper: + condition: service_healthy + bookie: + condition: service_started + ports: + - "6650:6650" + - "18080:8080" + command: bash -c "bin/apply-config-from-env.py conf/broker.conf && exec bin/pulsar broker" + + rabbitmq: + image: rabbitmq:3.13.3-management + container_name: 'rabbitmq' + ports: + - 5672:5672 + - 15672:15672 + volumes: + - rabbitmq/data/:/var/lib/rabbitmq/ + - rabbitmq/log/:/var/log/rabbitmq + - rabbitmq/plugins:/usr/lib/rabbitmq/plugins + - rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw + environment: + - RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/usr/lib/rabbitmq/plugins + networks: + - iot-net + + redis: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + volumes: + - redis/data:/data + networks: + - iot-net + + rmqnamesrv: + image: foxiswho/rocketmq:server + restart: always + container_name: rmqnamesrv + ports: + - 9876:9876 + volumes: + - rocketmq/rmqnamesrv/logs:/opt/logs + - rocketmq/rmqnamesrv/store:/opt/store + networks: + iot-net: + aliases: + - rmqnamesrv + + rmqbroker: + image: foxiswho/rocketmq:broker + restart: always + container_name: rmqbroker + ports: + - 10909:10909 + - 10911:10911 + volumes: + - rocketmq/rmqbroker/logs:/opt/logs + - rocketmq/rmqbroker/store:/opt/store + - rocketmq/rmqbroker/conf/broker.conf:/etc/rocketmq/broker.conf + environment: + NAMESRV_ADDR: "rmqnamesrv:9876" + JAVA_OPTS: " -Duser.home=/opt" + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + command: mqbroker -c /etc/rocketmq/broker.conf + depends_on: + - rmqnamesrv + networks: + iot-net: + aliases: + - rmqbroker + + rmqconsole: + image: styletang/rocketmq-console-ng + restart: always + container_name: rmqconsole + ports: + - 18080:8080 + environment: + JAVA_OPTS: "-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" + depends_on: + - rmqnamesrv + networks: + iot-net: + aliases: + - rmqconsole + + iotgomq: + image: go-iot-mq:latest + ports: + - 8001:19000 + networks: + - iot-net + iotgoproject: + image: go-iot-project:latest + ports: + - 8002:8080 + networks: + - iot-net + iotgomqtt: + image: go-iot-mqtt:latest + ports: + - 8003:8081 + networks: + - iot-net +volumes: + cassandra-data: + influxdbv2: + +networks: + iot-net: + driver: bridge + diff --git a/docker/iot/docker-compose.yml b/docker/iot/docker-compose.yml new file mode 100644 index 0000000..130d228 --- /dev/null +++ b/docker/iot/docker-compose.yml @@ -0,0 +1,13 @@ +services: + iotgomq: + image: go-iot-mq:latest + ports: + - 8001:19000 + iotgoproject: + image: go-iot-project:latest + ports: + - 8002:8080 + iotgomqtt: + image: go-iot-mqtt:latest + ports: + - 8003:8081 diff --git a/docker/mysql/docker-compose.yml b/docker/mysql/docker-compose.yml new file mode 100644 index 0000000..d9507bb --- /dev/null +++ b/docker/mysql/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + mysql: + image: mysql:8.0 + container_name: mysql8 + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: mysql + MYSQL_USER: iot + MYSQL_PASSWORD: iot123456 + TZ: "Asia/Shanghai" + volumes: + - ./data:/var/lib/mysql + ports: + - "3306:3306" + command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --binlog-format=ROW -- Gitee From d091a7ce60cb4163722922e4d236592728452e66 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 18 Jul 2024 21:44:07 +0800 Subject: [PATCH 02/90] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E7=9A=84docker-compose=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/{iot => app}/docker-compose.yml | 0 docker/docker-compose.yml | 393 ------------------ docker/{ => env}/Cassandra/docker-compose.yml | 0 docker/{ => env}/Pulsar/docker-compose.yml | 0 docker/{ => env}/clickhouse/config/config.xml | 0 .../config/docker_related_config.xml | 0 docker/{ => env}/clickhouse/config/users.xml | 0 .../{ => env}/clickhouse/docker-compose.yml | 0 docker/env/docker-compose.yml | 121 ++++++ docker/{ => env}/influx/docker-compose.yml | 0 docker/{ => env}/influx/influxv2.env | 0 .../{ => env}/influx/telegraf/mytelegraf.conf | 0 docker/{ => env}/iotdb/docker-compose.yml | 0 docker/{ => env}/kafka/docker-compose.yml | 0 docker/{ => env}/mongo/docker-compose.yml | 0 docker/{ => env}/mqtt/docker-compose.yml | 0 docker/{ => env}/mqtt/mock/Dockerfile | 0 docker/{ => env}/mqtt/mock/go.mod | 0 docker/{ => env}/mqtt/mock/go.sum | 0 docker/{ => env}/mqtt/mock/main.go | 0 docker/{ => env}/mqtt/mock/mqtt.yml | 0 docker/{ => env}/mqtt/mock/start.sh | 0 docker/{ => env}/mqtt/readme.md | 0 docker/{ => env}/mysql/docker-compose.yml | 2 +- docker/{ => env}/rabbitmq/Dockerfile | 0 docker/{ => env}/rabbitmq/docker-compose.yml | 0 docker/{ => env}/rabbitmq/enabled_plugins | 0 ...abbitmq_delayed_message_exchange-3.10.2.ez | Bin docker/{ => env}/redis/docker-compose.yml | 0 docker/{ => env}/rocketmq/docker-compose.yml | 0 docker/start.sh | 3 + 31 files changed, 125 insertions(+), 394 deletions(-) rename docker/{iot => app}/docker-compose.yml (100%) delete mode 100644 docker/docker-compose.yml rename docker/{ => env}/Cassandra/docker-compose.yml (100%) rename docker/{ => env}/Pulsar/docker-compose.yml (100%) rename docker/{ => env}/clickhouse/config/config.xml (100%) rename docker/{ => env}/clickhouse/config/docker_related_config.xml (100%) rename docker/{ => env}/clickhouse/config/users.xml (100%) rename docker/{ => env}/clickhouse/docker-compose.yml (100%) create mode 100644 docker/env/docker-compose.yml rename docker/{ => env}/influx/docker-compose.yml (100%) rename docker/{ => env}/influx/influxv2.env (100%) rename docker/{ => env}/influx/telegraf/mytelegraf.conf (100%) rename docker/{ => env}/iotdb/docker-compose.yml (100%) rename docker/{ => env}/kafka/docker-compose.yml (100%) rename docker/{ => env}/mongo/docker-compose.yml (100%) rename docker/{ => env}/mqtt/docker-compose.yml (100%) rename docker/{ => env}/mqtt/mock/Dockerfile (100%) rename docker/{ => env}/mqtt/mock/go.mod (100%) rename docker/{ => env}/mqtt/mock/go.sum (100%) rename docker/{ => env}/mqtt/mock/main.go (100%) rename docker/{ => env}/mqtt/mock/mqtt.yml (100%) rename docker/{ => env}/mqtt/mock/start.sh (100%) rename docker/{ => env}/mqtt/readme.md (100%) rename docker/{ => env}/mysql/docker-compose.yml (95%) rename docker/{ => env}/rabbitmq/Dockerfile (100%) rename docker/{ => env}/rabbitmq/docker-compose.yml (100%) rename docker/{ => env}/rabbitmq/enabled_plugins (100%) rename docker/{ => env}/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez (100%) rename docker/{ => env}/redis/docker-compose.yml (100%) rename docker/{ => env}/rocketmq/docker-compose.yml (100%) create mode 100644 docker/start.sh diff --git a/docker/iot/docker-compose.yml b/docker/app/docker-compose.yml similarity index 100% rename from docker/iot/docker-compose.yml rename to docker/app/docker-compose.yml diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index fa92010..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,393 +0,0 @@ -version: '3' - -services: - cassandra: - image: cassandra:latest - container_name: cassandra-container - ports: - - "9042:9042" - environment: - - CASSANDRA_USER=admin - - CASSANDRA_PASSWORD=admin - volumes: - - Cassandra/dataa:/var/lib/cassandra - networks: - - iot-net - clickhouse: - image: yandex/clickhouse-server:22.1.3.7 - container_name: clickhouse - restart: always - ports: - - "8123:8123" - - "9000:9000" - volumes: - # 默认配置 - - clickhouse/config/docker_related_config.xml:/etc/clickhouse-server/config.d/docker_related_config.xml:rw - - clickhouse/config/config.xml:/etc/clickhouse-server/config.xml:rw - - clickhouse/config/users.xml:/etc/clickhouse-server/users.xml:rw - - /etc/localtime:/etc/localtime:ro - # 运行日志 - - clickhouse/log:/var/log/clickhouse-server - # 数据持久 - - clickhouse/data:/var/lib/clickhouse:rw - networks: - - iot-net - influxdb: - image: influxdb:2.6-alpine - env_file: - - influxv2.env - volumes: - # Mount for influxdb data directory and configuration - - influxdbv2:/var/lib/influxdb2:rw - ports: - - "8086:8086" - networks: - - iot-net - telegraf: - image: telegraf:1.25-alpine - depends_on: - - influxdb - volumes: - # Mount for telegraf config - - influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro - env_file: - - influx/influxv2.env - networks: - - iot-net - iotdb-service: - image: apache/iotdb:1.3.0-standalone - hostname: iotdb-service - container_name: iotdb-service - ports: - - "6667:6667" - environment: - - cn_internal_address=iotdb-service - - cn_internal_port=10710 - - cn_consensus_port=10720 - - cn_seed_config_node=iotdb-service:10710 - - dn_rpc_address=iotdb-service - - dn_internal_address=iotdb-service - - dn_rpc_port=6667 - - dn_mpp_data_exchange_port=10740 - - dn_schema_region_consensus_port=10750 - - dn_data_region_consensus_port=10760 - - dn_seed_config_node=iotdb-service:10710 - volumes: - - ./data:/iotdb/data - - ./logs:/iotdb/logs - networks: - - iot-net -# zookeeper: -# image: wurstmeister/zookeeper -# container_name: zookeeper -# ports: -# - "2181:2181" - kafka: - image: wurstmeister/kafka - container_name: kafka - volumes: - - /etc/localtime:/etc/localtime - ports: - - "9092:9092" - environment: - KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_PORT: 9092 - KAFKA_LOG_RETENTION_HOURS: 120 - KAFKA_MESSAGE_MAX_BYTES: 10000000 - KAFKA_REPLICA_FETCH_MAX_BYTES: 10000000 - KAFKA_GROUP_MAX_SESSION_TIMEOUT_MS: 60000 - KAFKA_NUM_PARTITIONS: 3 - KAFKA_DELETE_RETENTION_MS: 1000 - networks: - - iot-net - kafka-manager: - image: sheepkiller/kafka-manager - container_name: kafka-manager - environment: - ZK_HOSTS: 127.0.0.1 - ports: - - "9009:9000" - networks: - - iot-net - mongodb: - image: mongo - container_name: mongodb - ports: - - 27017:27017 - volumes: - - ./database:/data/db - environment: - - MONGO_INITDB_ROOT_USERNAME=admin - - MONGO_INITDB_ROOT_PASSWORD=admin - networks: - - iot-net - mongo-express: - image: mongo-express - container_name: mongo-express - restart: always - ports: - - 8181:8081 - environment: - - ME_CONFIG_MONGODB_ADMINUSERNAME=admin - - ME_CONFIG_MONGODB_ADMINPASSWORD=admin - - ME_CONFIG_MONGODB_SERVER=mongodb - networks: - - iot-net - emqx1: - image: emqx:5.4.1 - container_name: emqx1 - environment: - - "EMQX_NODE_NAME=emqx@node1.emqx.io" - - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" - - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" - healthcheck: - test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] - interval: 5s - timeout: 25s - retries: 5 - networks: - iot-net: - aliases: - - node1.emqx.io - ports: - - 1883:1883 - - 8083:8083 - - 8084:8084 - - 8883:8883 - - 18083:18083 - # volumes: - # - $PWD/emqx1_data:/opt/emqx/data - - emqx2: - image: emqx:5.4.1 - container_name: emqx2 - environment: - - "EMQX_NODE_NAME=emqx@node2.emqx.io" - - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" - - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" - healthcheck: - test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] - interval: 5s - timeout: 25s - retries: 5 - networks: - iot-net: - aliases: - - node2.emqx.io - mysql: - image: mysql:8.0 - container_name: mysql8 - restart: always - environment: - MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: mysql - MYSQL_USER: iot - MYSQL_PASSWORD: iot123456 - TZ: "Asia/Shanghai" - volumes: - - mysql/data:/var/lib/mysql - ports: - - "3306:3306" - command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --binlog-format=ROW - networks: - - iot-net - - # Start zookeeper - zookeeper: - image: apachepulsar/pulsar:latest - container_name: zookeeper - restart: on-failure - networks: - - iot-net - volumes: - - Pulsar/data/zookeeper:/pulsar/data/zookeeper - environment: - - metadataStoreUrl=zk:zookeeper:2181 - - PULSAR_MEM=-Xms256m -Xmx256m -XX:MaxDirectMemorySize=256m - command: > - bash -c "bin/apply-config-from-env.py conf/zookeeper.conf && \ - bin/generate-zookeeper-config.sh conf/zookeeper.conf && \ - exec bin/pulsar zookeeper" - healthcheck: - test: ["CMD", "bin/pulsar-zookeeper-ruok.sh"] - interval: 10s - timeout: 5s - retries: 30 - - # Init cluster metadata - pulsar-init: - container_name: pulsar-init - hostname: pulsar-init - image: apachepulsar/pulsar:latest - networks: - - iot-net - command: > - bin/pulsar initialize-cluster-metadata \ - --cluster cluster-a \ - --zookeeper zookeeper:2181 \ - --configuration-store zookeeper:2181 \ - --web-service-url http://broker:8080 \ - --broker-service-url pulsar://broker:6650 - depends_on: - zookeeper: - condition: service_healthy - - # Start bookie - bookie: - image: apachepulsar/pulsar:latest - container_name: bookie - restart: on-failure - networks: - - iot-net - environment: - - clusterName=cluster-a - - zkServers=zookeeper:2181 - - metadataServiceUri=metadata-store:zk:zookeeper:2181 - # otherwise every time we run docker compose uo or down we fail to start due to Cookie - # See: https://github.com/apache/bookkeeper/blob/405e72acf42bb1104296447ea8840d805094c787/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java#L57-68 - - advertisedAddress=bookie - - BOOKIE_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m - depends_on: - zookeeper: - condition: service_healthy - pulsar-init: - condition: service_completed_successfully - # Map the local directory to the container to avoid bookie startup failure due to insufficient container disks. - volumes: - - Pulsar/data/bookkeeper:/pulsar/data/bookkeeper - command: bash -c "bin/apply-config-from-env.py conf/bookkeeper.conf && exec bin/pulsar bookie" - - # Start broker - broker: - image: apachepulsar/pulsar:latest - container_name: broker - hostname: broker - restart: on-failure - networks: - - iot-net - environment: - - metadataStoreUrl=zk:zookeeper:2181 - - zookeeperServers=zookeeper:2181 - - clusterName=cluster-a - - managedLedgerDefaultEnsembleSize=1 - - managedLedgerDefaultWriteQuorum=1 - - managedLedgerDefaultAckQuorum=1 - - advertisedAddress=broker - - advertisedListeners=external:pulsar://127.0.0.1:6650 - - PULSAR_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m - depends_on: - zookeeper: - condition: service_healthy - bookie: - condition: service_started - ports: - - "6650:6650" - - "18080:8080" - command: bash -c "bin/apply-config-from-env.py conf/broker.conf && exec bin/pulsar broker" - - rabbitmq: - image: rabbitmq:3.13.3-management - container_name: 'rabbitmq' - ports: - - 5672:5672 - - 15672:15672 - volumes: - - rabbitmq/data/:/var/lib/rabbitmq/ - - rabbitmq/log/:/var/log/rabbitmq - - rabbitmq/plugins:/usr/lib/rabbitmq/plugins - - rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw - environment: - - RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/usr/lib/rabbitmq/plugins - networks: - - iot-net - - redis: - image: redis:6.2-alpine - restart: always - ports: - - '6379:6379' - command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 - volumes: - - redis/data:/data - networks: - - iot-net - - rmqnamesrv: - image: foxiswho/rocketmq:server - restart: always - container_name: rmqnamesrv - ports: - - 9876:9876 - volumes: - - rocketmq/rmqnamesrv/logs:/opt/logs - - rocketmq/rmqnamesrv/store:/opt/store - networks: - iot-net: - aliases: - - rmqnamesrv - - rmqbroker: - image: foxiswho/rocketmq:broker - restart: always - container_name: rmqbroker - ports: - - 10909:10909 - - 10911:10911 - volumes: - - rocketmq/rmqbroker/logs:/opt/logs - - rocketmq/rmqbroker/store:/opt/store - - rocketmq/rmqbroker/conf/broker.conf:/etc/rocketmq/broker.conf - environment: - NAMESRV_ADDR: "rmqnamesrv:9876" - JAVA_OPTS: " -Duser.home=/opt" - JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" - command: mqbroker -c /etc/rocketmq/broker.conf - depends_on: - - rmqnamesrv - networks: - iot-net: - aliases: - - rmqbroker - - rmqconsole: - image: styletang/rocketmq-console-ng - restart: always - container_name: rmqconsole - ports: - - 18080:8080 - environment: - JAVA_OPTS: "-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" - depends_on: - - rmqnamesrv - networks: - iot-net: - aliases: - - rmqconsole - - iotgomq: - image: go-iot-mq:latest - ports: - - 8001:19000 - networks: - - iot-net - iotgoproject: - image: go-iot-project:latest - ports: - - 8002:8080 - networks: - - iot-net - iotgomqtt: - image: go-iot-mqtt:latest - ports: - - 8003:8081 - networks: - - iot-net -volumes: - cassandra-data: - influxdbv2: - -networks: - iot-net: - driver: bridge - diff --git a/docker/Cassandra/docker-compose.yml b/docker/env/Cassandra/docker-compose.yml similarity index 100% rename from docker/Cassandra/docker-compose.yml rename to docker/env/Cassandra/docker-compose.yml diff --git a/docker/Pulsar/docker-compose.yml b/docker/env/Pulsar/docker-compose.yml similarity index 100% rename from docker/Pulsar/docker-compose.yml rename to docker/env/Pulsar/docker-compose.yml diff --git a/docker/clickhouse/config/config.xml b/docker/env/clickhouse/config/config.xml similarity index 100% rename from docker/clickhouse/config/config.xml rename to docker/env/clickhouse/config/config.xml diff --git a/docker/clickhouse/config/docker_related_config.xml b/docker/env/clickhouse/config/docker_related_config.xml similarity index 100% rename from docker/clickhouse/config/docker_related_config.xml rename to docker/env/clickhouse/config/docker_related_config.xml diff --git a/docker/clickhouse/config/users.xml b/docker/env/clickhouse/config/users.xml similarity index 100% rename from docker/clickhouse/config/users.xml rename to docker/env/clickhouse/config/users.xml diff --git a/docker/clickhouse/docker-compose.yml b/docker/env/clickhouse/docker-compose.yml similarity index 100% rename from docker/clickhouse/docker-compose.yml rename to docker/env/clickhouse/docker-compose.yml diff --git a/docker/env/docker-compose.yml b/docker/env/docker-compose.yml new file mode 100644 index 0000000..25b90ae --- /dev/null +++ b/docker/env/docker-compose.yml @@ -0,0 +1,121 @@ +version: '3' + +services: + influxdb: + image: influxdb:2.6-alpine + env_file: + - influxv2.env + volumes: + # Mount for influxdb data directory and configuration + - influxdbv2:/var/lib/influxdb2:rw + ports: + - "8086:8086" + networks: + - iot-net + telegraf: + image: telegraf:1.25-alpine + depends_on: + - influxdb + volumes: + # Mount for telegraf config + - influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro + env_file: + - influx/influxv2.env + networks: + - iot-net + mongodb: + image: mongo + container_name: mongodb + ports: + - 27017:27017 + volumes: + - ./database:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + networks: + - iot-net + mongo-express: + image: mongo-express + container_name: mongo-express + restart: always + ports: + - 8181:8081 + environment: + - ME_CONFIG_MONGODB_ADMINUSERNAME=admin + - ME_CONFIG_MONGODB_ADMINPASSWORD=admin + - ME_CONFIG_MONGODB_SERVER=mongodb + networks: + - iot-net + emqx1: + image: emqx:5.4.1 + container_name: emqx1 + environment: + - "EMQX_NODE_NAME=emqx@node1.emqx.io" + - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" + - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" + healthcheck: + test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] + interval: 5s + timeout: 25s + retries: 5 + networks: + iot-net: + aliases: + - node1.emqx.io + ports: + - 1883:1883 + - 8083:8083 + - 8084:8084 + - 8883:8883 + - 18083:18083 + mysql: + image: mysql:8.0 + container_name: mysql8 + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: mysql + MYSQL_USER: app + MYSQL_PASSWORD: iot123456 + TZ: "Asia/Shanghai" + volumes: + - mysql/data:/var/lib/mysql + ports: + - "3306:3306" + command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --binlog-format=ROW + networks: + - iot-net + rabbitmq: + image: rabbitmq:3.13.3-management + container_name: 'rabbitmq' + ports: + - 5672:5672 + - 15672:15672 + volumes: + - rabbitmq/data/:/var/lib/rabbitmq/ + - rabbitmq/log/:/var/log/rabbitmq + - rabbitmq/plugins:/usr/lib/rabbitmq/plugins + - rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw + environment: + - RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/usr/lib/rabbitmq/plugins + networks: + - iot-net + + redis: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + volumes: + - redis/data:/data + networks: + - iot-net +volumes: + influxdbv2: + +networks: + iot-net: + driver: bridge + diff --git a/docker/influx/docker-compose.yml b/docker/env/influx/docker-compose.yml similarity index 100% rename from docker/influx/docker-compose.yml rename to docker/env/influx/docker-compose.yml diff --git a/docker/influx/influxv2.env b/docker/env/influx/influxv2.env similarity index 100% rename from docker/influx/influxv2.env rename to docker/env/influx/influxv2.env diff --git a/docker/influx/telegraf/mytelegraf.conf b/docker/env/influx/telegraf/mytelegraf.conf similarity index 100% rename from docker/influx/telegraf/mytelegraf.conf rename to docker/env/influx/telegraf/mytelegraf.conf diff --git a/docker/iotdb/docker-compose.yml b/docker/env/iotdb/docker-compose.yml similarity index 100% rename from docker/iotdb/docker-compose.yml rename to docker/env/iotdb/docker-compose.yml diff --git a/docker/kafka/docker-compose.yml b/docker/env/kafka/docker-compose.yml similarity index 100% rename from docker/kafka/docker-compose.yml rename to docker/env/kafka/docker-compose.yml diff --git a/docker/mongo/docker-compose.yml b/docker/env/mongo/docker-compose.yml similarity index 100% rename from docker/mongo/docker-compose.yml rename to docker/env/mongo/docker-compose.yml diff --git a/docker/mqtt/docker-compose.yml b/docker/env/mqtt/docker-compose.yml similarity index 100% rename from docker/mqtt/docker-compose.yml rename to docker/env/mqtt/docker-compose.yml diff --git a/docker/mqtt/mock/Dockerfile b/docker/env/mqtt/mock/Dockerfile similarity index 100% rename from docker/mqtt/mock/Dockerfile rename to docker/env/mqtt/mock/Dockerfile diff --git a/docker/mqtt/mock/go.mod b/docker/env/mqtt/mock/go.mod similarity index 100% rename from docker/mqtt/mock/go.mod rename to docker/env/mqtt/mock/go.mod diff --git a/docker/mqtt/mock/go.sum b/docker/env/mqtt/mock/go.sum similarity index 100% rename from docker/mqtt/mock/go.sum rename to docker/env/mqtt/mock/go.sum diff --git a/docker/mqtt/mock/main.go b/docker/env/mqtt/mock/main.go similarity index 100% rename from docker/mqtt/mock/main.go rename to docker/env/mqtt/mock/main.go diff --git a/docker/mqtt/mock/mqtt.yml b/docker/env/mqtt/mock/mqtt.yml similarity index 100% rename from docker/mqtt/mock/mqtt.yml rename to docker/env/mqtt/mock/mqtt.yml diff --git a/docker/mqtt/mock/start.sh b/docker/env/mqtt/mock/start.sh similarity index 100% rename from docker/mqtt/mock/start.sh rename to docker/env/mqtt/mock/start.sh diff --git a/docker/mqtt/readme.md b/docker/env/mqtt/readme.md similarity index 100% rename from docker/mqtt/readme.md rename to docker/env/mqtt/readme.md diff --git a/docker/mysql/docker-compose.yml b/docker/env/mysql/docker-compose.yml similarity index 95% rename from docker/mysql/docker-compose.yml rename to docker/env/mysql/docker-compose.yml index d9507bb..b281053 100644 --- a/docker/mysql/docker-compose.yml +++ b/docker/env/mysql/docker-compose.yml @@ -8,7 +8,7 @@ services: environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: mysql - MYSQL_USER: iot + MYSQL_USER: app MYSQL_PASSWORD: iot123456 TZ: "Asia/Shanghai" volumes: diff --git a/docker/rabbitmq/Dockerfile b/docker/env/rabbitmq/Dockerfile similarity index 100% rename from docker/rabbitmq/Dockerfile rename to docker/env/rabbitmq/Dockerfile diff --git a/docker/rabbitmq/docker-compose.yml b/docker/env/rabbitmq/docker-compose.yml similarity index 100% rename from docker/rabbitmq/docker-compose.yml rename to docker/env/rabbitmq/docker-compose.yml diff --git a/docker/rabbitmq/enabled_plugins b/docker/env/rabbitmq/enabled_plugins similarity index 100% rename from docker/rabbitmq/enabled_plugins rename to docker/env/rabbitmq/enabled_plugins diff --git a/docker/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez b/docker/env/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez similarity index 100% rename from docker/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez rename to docker/env/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez diff --git a/docker/redis/docker-compose.yml b/docker/env/redis/docker-compose.yml similarity index 100% rename from docker/redis/docker-compose.yml rename to docker/env/redis/docker-compose.yml diff --git a/docker/rocketmq/docker-compose.yml b/docker/env/rocketmq/docker-compose.yml similarity index 100% rename from docker/rocketmq/docker-compose.yml rename to docker/env/rocketmq/docker-compose.yml diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..07d28c3 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,3 @@ +#!/bin/zsh + docker-compose -f env/docker-compose.yml up -f app/docker-compose.yml -d + echo "执行完毕,项目启动中..." -- Gitee From 6a02e6eefed6c993b9b3382b7a0f420dca1f73a1 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Fri, 19 Jul 2024 11:25:20 +0800 Subject: [PATCH 03/90] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E7=9A=84docker-compose=E6=96=87?= =?UTF-8?q?=E4=BB=B6,=E4=BF=AE=E6=94=B9go-iot-mq=E9=81=97=E6=BC=8F?= =?UTF-8?q?=E7=9A=84=E6=9C=AA=E4=BD=BF=E7=94=A8=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=9A=84rabbitmq=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/IotGoMQ.Dockerfile | 3 +- docker/app/docker-compose.yml | 71 +++++- docker/app/iot-project/config/app-local.yml | 35 +++ docker/app/mq/config/app-local-calc.yml | 35 +++ .../app/mq/config/app-local-pre_handler.yml | 34 +++ .../mq/config/app-local-waring_handler.yml | 34 +++ docker/app/mq/config/app-local-wd.yml | 34 +++ docker/app/mqtt/config/app-local.yml | 19 ++ docker/app/mqtt/config/app-local2.yml | 19 ++ docker/app/mqtt/config/app-local3.yml | 19 ++ ...ompose.yml => base-env-docker-compose.yml} | 0 docker/env/big-data-env-docker-compose.yml | 228 ++++++++++++++++++ docker/start.sh | 2 +- go-iot-mq/log.go | 14 +- go-iot-mq/mq.go | 2 +- go-iot/log.go | 14 +- iot-go-project/initialize/init.go | 24 +- 17 files changed, 546 insertions(+), 41 deletions(-) create mode 100644 docker/app/iot-project/config/app-local.yml create mode 100644 docker/app/mq/config/app-local-calc.yml create mode 100644 docker/app/mq/config/app-local-pre_handler.yml create mode 100644 docker/app/mq/config/app-local-waring_handler.yml create mode 100644 docker/app/mq/config/app-local-wd.yml create mode 100644 docker/app/mqtt/config/app-local.yml create mode 100644 docker/app/mqtt/config/app-local2.yml create mode 100644 docker/app/mqtt/config/app-local3.yml rename docker/env/{docker-compose.yml => base-env-docker-compose.yml} (100%) create mode 100644 docker/env/big-data-env-docker-compose.yml diff --git a/deploy/IotGoMQ.Dockerfile b/deploy/IotGoMQ.Dockerfile index 0f3ea03..2c3bc0f 100644 --- a/deploy/IotGoMQ.Dockerfile +++ b/deploy/IotGoMQ.Dockerfile @@ -9,7 +9,6 @@ WORKDIR /app COPY ../go-iot-mq ./go-iot-mq COPY ../notice ./notice COPY ../transmit ./transmit -COPY ../go-iot ./go-iot # RUN cd go-iot-mq && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . @@ -32,4 +31,4 @@ ENV GIN_MODE=release \ EXPOSE 8080 #fixme: 配置需要动态调整 -ENTRYPOINT ["/app/main", "-config", "/app/app-local.yml"] +ENTRYPOINT ["/app/main","-config","/app/app-local.yml"] diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index 130d228..b550331 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -1,13 +1,76 @@ services: - iotgomq: + iotgomq-pre_handler: + build: + context: ../../ + dockerfile: deploy/IotGoMQ.Dockerfile image: go-iot-mq:latest ports: - 8001:19000 + volumes: + - ./mq/config/app-local-pre_handler.yml:/app/app-local-pre_handler.yml + entrypoint: ["/app/main", "-config", "/app/app-local-pre_handler.yml"] + networks: + - iot-net + iotgomq-calc_handler: + image: go-iot-mq:latest + ports: + - 8002:19000 + volumes: + - ./mq/config/app-local-calc.yml:/app/app-local-calc.yml + entrypoint: ["/app/main", "-config", "/app/app-local-calc.yml"] + networks: + - iot-net + iotgomq-waring_handler: + image: go-iot-mq:latest + ports: + - 8003:19000 + volumes: + - ./mq/config/app-local-waring_handler.yml:/app/app-local-waring_handler.yml + entrypoint: ["/app/main", "-config", "/app/app-local-waring_handler.yml"] + networks: + - iot-net + iotgomq-wd_handler: + image: go-iot-mq:latest + ports: + - 8004:19000 + volumes: + - ./mq/config/app-local-wd.yml:/app/app-local-wd.yml + entrypoint: ["/app/main", "-config", "/app/app-local-wd.yml"] + networks: + - iot-net iotgoproject: + build: + context: ../../ + dockerfile: deploy/IotGoProject.Dockerfile image: go-iot-project:latest ports: - - 8002:8080 - iotgomqtt: + - 8005:8080 + networks: + - iot-net + iotgomqtt1: + build: + context: ../../ + dockerfile: deploy/IotMQTT.Dockerfile + image: go-iot-mqtt:latest + entrypoint: ["/app/main", "-config", "/app/app-local.yml"] + ports: + - 8006:8081 + networks: + - iot-net + iotgomqtt2: + image: go-iot-mqtt:latest + ports: + - 8007:8081 + entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] + networks: + - iot-net + iotgomqtt3: image: go-iot-mqtt:latest ports: - - 8003:8081 + - 8008:8081 + entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] + networks: + - iot-net +networks: + iot-net: + driver: bridge diff --git a/docker/app/iot-project/config/app-local.yml b/docker/app/iot-project/config/app-local.yml new file mode 100644 index 0000000..212246e --- /dev/null +++ b/docker/app/iot-project/config/app-local.yml @@ -0,0 +1,35 @@ +node_info: + port: 8080 +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest +influx_config: + host: 192.168.7.41 + port: 8086 + token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + org: myorg + bucket: buc + +mysql_config: + username: root + password: root123@ + host: 192.168.7.41 + port: 3306 + dbname: iot +mongo_config: + host: 192.168.7.41 + port: 27017 + username: admin + password: admin + db: iot + collection: calc + waring_collection: waring + script_waring_collection: script_waring diff --git a/docker/app/mq/config/app-local-calc.yml b/docker/app/mq/config/app-local-calc.yml new file mode 100644 index 0000000..b9391db --- /dev/null +++ b/docker/app/mq/config/app-local-calc.yml @@ -0,0 +1,35 @@ +node_info: + host: 192.168.7.41 + port: 29001 + name: mq1 + type: calc_queue # pre_handler、 waring_handler、 calc_queue、waring_delay_handler + + +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest +influx_config: + host: 192.168.7.41 + port: 8086 + token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + org: myorg + bucket: buc +mongo_config: + host: 192.168.7.41 + port: 27017 + username: admin + password: admin + db: iot + collection: calc + waring_collection: waring + script_waring_collection: script_waring diff --git a/docker/app/mq/config/app-local-pre_handler.yml b/docker/app/mq/config/app-local-pre_handler.yml new file mode 100644 index 0000000..009252a --- /dev/null +++ b/docker/app/mq/config/app-local-pre_handler.yml @@ -0,0 +1,34 @@ +node_info: + host: 192.168.7.41 + port: 29002 + name: mq1 + type: pre_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler + + +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest +influx_config: + host: 192.168.7.41 + port: 8086 + token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + org: myorg + bucket: buc +mongo_config: + host: 192.168.7.111 + port: 27017 + username: admin + password: admin + db: iot + collection: calc + waring_collection: waring + script_waring_collection: script_waring diff --git a/docker/app/mq/config/app-local-waring_handler.yml b/docker/app/mq/config/app-local-waring_handler.yml new file mode 100644 index 0000000..57ff2e6 --- /dev/null +++ b/docker/app/mq/config/app-local-waring_handler.yml @@ -0,0 +1,34 @@ +node_info: + host: 192.168.7.41 + port: 29003 + name: mq1 + type: waring_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler + + +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest +influx_config: + host: 192.168.7.41 + port: 8086 + token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + org: myorg + bucket: buc +mongo_config: + host: 192.168.7.41 + port: 27017 + username: admin + password: admin + db: iot + collection: calc + waring_collection: waring + script_waring_collection: script_waring diff --git a/docker/app/mq/config/app-local-wd.yml b/docker/app/mq/config/app-local-wd.yml new file mode 100644 index 0000000..afefdeb --- /dev/null +++ b/docker/app/mq/config/app-local-wd.yml @@ -0,0 +1,34 @@ +node_info: + host: 192.168.7.41 + port: 29004 + name: mq1 + type: waring_delay_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler + + +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest +influx_config: + host: 192.168.7.41 + port: 8086 + token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + org: myorg + bucket: buc +mongo_config: + host: 192.168.7.41 + port: 27017 + username: admin + password: admin + db: iot + collection: calc + waring_collection: waring + script_waring_collection: script_waring diff --git a/docker/app/mqtt/config/app-local.yml b/docker/app/mqtt/config/app-local.yml new file mode 100644 index 0000000..8a12bd9 --- /dev/null +++ b/docker/app/mqtt/config/app-local.yml @@ -0,0 +1,19 @@ +node_info: + host: 192.168.7.41 + port: 8081 + name: m1 + type: mqtt + size: 3 +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest diff --git a/docker/app/mqtt/config/app-local2.yml b/docker/app/mqtt/config/app-local2.yml new file mode 100644 index 0000000..c0bf36f --- /dev/null +++ b/docker/app/mqtt/config/app-local2.yml @@ -0,0 +1,19 @@ +node_info: + host: 192.168.7.41 + port: 8082 + name: m2 + type: mqtt + size: 3 +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest diff --git a/docker/app/mqtt/config/app-local3.yml b/docker/app/mqtt/config/app-local3.yml new file mode 100644 index 0000000..0b8e08f --- /dev/null +++ b/docker/app/mqtt/config/app-local3.yml @@ -0,0 +1,19 @@ +node_info: + host: 192.168.7.41 + port: 8081 + name: m3 + type: mqtt + size: 3 +redis_config: + host: 192.168.7.41 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 192.168.7.41 + port: 5672 + username: guest + password: guest diff --git a/docker/env/docker-compose.yml b/docker/env/base-env-docker-compose.yml similarity index 100% rename from docker/env/docker-compose.yml rename to docker/env/base-env-docker-compose.yml diff --git a/docker/env/big-data-env-docker-compose.yml b/docker/env/big-data-env-docker-compose.yml new file mode 100644 index 0000000..8b3f94d --- /dev/null +++ b/docker/env/big-data-env-docker-compose.yml @@ -0,0 +1,228 @@ +version: '3' +services: + cassandra: + image: cassandra:latest + container_name: cassandra-container + ports: + - "9042:9042" + environment: + - CASSANDRA_USER=admin + - CASSANDRA_PASSWORD=admin + volumes: + - ./dataa:/var/lib/cassandra + clickhouse: + image: yandex/clickhouse-server:22.1.3.7 + container_name: clickhouse + restart: always + ports: + - "8123:8123" + - "9000:9000" + volumes: + # 默认配置 + - clickhouse/config/docker_related_config.xml:/etc/clickhouse-server/config.d/docker_related_config.xml:rw + - clickhouse/config/config.xml:/etc/clickhouse-server/config.xml:rw + - clickhouse/config/users.xml:/etc/clickhouse-server/users.xml:rw + - /etc/localtime:/etc/localtime:ro + # 运行日志 + - clickhouse/log:/var/log/clickhouse-server + # 数据持久 + - clickhouse/data:/var/lib/clickhouse:rw + iotdb-service: + image: apache/iotdb:1.3.0-standalone + hostname: iotdb-service + container_name: iotdb-service + ports: + - "6667:6667" + environment: + - cn_internal_address=iotdb-service + - cn_internal_port=10710 + - cn_consensus_port=10720 + - cn_seed_config_node=iotdb-service:10710 + - dn_rpc_address=iotdb-service + - dn_internal_address=iotdb-service + - dn_rpc_port=6667 + - dn_mpp_data_exchange_port=10740 + - dn_schema_region_consensus_port=10750 + - dn_data_region_consensus_port=10760 + - dn_seed_config_node=iotdb-service:10710 + volumes: + - iotdb/data:/iotdb/data + - iotdb/logs:/iotdb/logs + networks: + iot-env: + zookeeper: + image: apachepulsar/pulsar:latest + container_name: zookeeper + restart: on-failure + networks: + - pulsar + volumes: + - Pulsar/data/zookeeper:/pulsar/data/zookeeper + environment: + - metadataStoreUrl=zk:zookeeper:2181 + - PULSAR_MEM=-Xms256m -Xmx256m -XX:MaxDirectMemorySize=256m + command: > + bash -c "bin/apply-config-from-env.py conf/zookeeper.conf && \ + bin/generate-zookeeper-config.sh conf/zookeeper.conf && \ + exec bin/pulsar zookeeper" + healthcheck: + test: ["CMD", "bin/pulsar-zookeeper-ruok.sh"] + interval: 10s + timeout: 5s + retries: 30 + + # Init cluster metadata + pulsar-init: + container_name: pulsar-init + hostname: pulsar-init + image: apachepulsar/pulsar:latest + networks: + - pulsar + command: > + bin/pulsar initialize-cluster-metadata \ + --cluster cluster-a \ + --zookeeper zookeeper:2181 \ + --configuration-store zookeeper:2181 \ + --web-service-url http://broker:8080 \ + --broker-service-url pulsar://broker:6650 + depends_on: + zookeeper: + condition: service_healthy + + # Start bookie + bookie: + image: apachepulsar/pulsar:latest + container_name: bookie + restart: on-failure + networks: + - pulsar + environment: + - clusterName=cluster-a + - zkServers=zookeeper:2181 + - metadataServiceUri=metadata-store:zk:zookeeper:2181 + # otherwise every time we run docker compose uo or down we fail to start due to Cookie + # See: https://github.com/apache/bookkeeper/blob/405e72acf42bb1104296447ea8840d805094c787/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java#L57-68 + - advertisedAddress=bookie + - BOOKIE_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m + depends_on: + zookeeper: + condition: service_healthy + pulsar-init: + condition: service_completed_successfully + # Map the local directory to the container to avoid bookie startup failure due to insufficient container disks. + volumes: + - Pulsar/data/bookkeeper:/pulsar/data/bookkeeper + command: bash -c "bin/apply-config-from-env.py conf/bookkeeper.conf && exec bin/pulsar bookie" + + # Start broker + broker: + image: apachepulsar/pulsar:latest + container_name: broker + hostname: broker + restart: on-failure + networks: + - pulsar + environment: + - metadataStoreUrl=zk:zookeeper:2181 + - zookeeperServers=zookeeper:2181 + - clusterName=cluster-a + - managedLedgerDefaultEnsembleSize=1 + - managedLedgerDefaultWriteQuorum=1 + - managedLedgerDefaultAckQuorum=1 + - advertisedAddress=broker + - advertisedListeners=external:pulsar://127.0.0.1:6650 + - PULSAR_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m + depends_on: + zookeeper: + condition: service_healthy + bookie: + condition: service_started + ports: + - "6650:6650" + - "18080:8080" + command: bash -c "bin/apply-config-from-env.py conf/broker.conf && exec bin/pulsar broker" + + kafka: + image: wurstmeister/kafka + container_name: kafka + volumes: + - /etc/localtime:/etc/localtime + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_PORT: 9092 + KAFKA_LOG_RETENTION_HOURS: 120 + KAFKA_MESSAGE_MAX_BYTES: 10000000 + KAFKA_REPLICA_FETCH_MAX_BYTES: 10000000 + KAFKA_GROUP_MAX_SESSION_TIMEOUT_MS: 60000 + KAFKA_NUM_PARTITIONS: 3 + KAFKA_DELETE_RETENTION_MS: 1000 + kafka-manager: + image: sheepkiller/kafka-manager + container_name: kafka-manager + environment: + ZK_HOSTS: 127.0.0.1 + ports: + - "9009:9000" + + rmqnamesrv: + image: foxiswho/rocketmq:server + restart: always + container_name: rmqnamesrv + ports: + - 9876:9876 + volumes: + - rocketmq/rmqnamesrv/logs:/opt/logs + - rocketmq/rmqnamesrv/store:/opt/store + networks: + rmq: + aliases: + - rmqnamesrv + + rmqbroker: + image: foxiswho/rocketmq:broker + restart: always + container_name: rmqbroker + ports: + - 10909:10909 + - 10911:10911 + volumes: + - rocketmq/rmqbroker/logs:/opt/logs + - rocketmq/rmqbroker/store:/opt/store + - rocketmq/rmqbroker/conf/broker.conf:/etc/rocketmq/broker.conf + environment: + NAMESRV_ADDR: "rmqnamesrv:9876" + JAVA_OPTS: " -Duser.home=/opt" + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + command: mqbroker -c /etc/rocketmq/broker.conf + depends_on: + - rmqnamesrv + networks: + rmq: + aliases: + - rmqbroker + + rmqconsole: + image: styletang/rocketmq-console-ng + restart: always + container_name: rmqconsole + ports: + - 18080:8080 + environment: + JAVA_OPTS: "-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" + depends_on: + - rmqnamesrv + networks: + rmq: + aliases: + - rmqconsole + +volumes: + cassandra-data: +networks: + iot-env: + driver: bridge + rmq: + driver: bridge diff --git a/docker/start.sh b/docker/start.sh index 07d28c3..eb04a96 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,3 +1,3 @@ #!/bin/zsh - docker-compose -f env/docker-compose.yml up -f app/docker-compose.yml -d + docker-compose -f env/base-env-docker-compose.yml up -f app/docker-compose.yml -d echo "执行完毕,项目启动中..." diff --git a/go-iot-mq/log.go b/go-iot-mq/log.go index b2e6a90..b79af21 100644 --- a/go-iot-mq/log.go +++ b/go-iot-mq/log.go @@ -1,11 +1,9 @@ package main import ( - "errors" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" - "syscall" "time" ) @@ -41,12 +39,12 @@ func InitLog() { zap.ReplaceGlobals(logger) // 替换全局 Logger // 确保日志被刷新 - defer func(logger *zap.Logger) { - err := logger.Sync() - if err != nil && !errors.Is(err, syscall.ENOTTY) { - zap.S().Errorf("日志同步失败 %+v", err) - } - }(logger) + //defer func(logger *zap.Logger) { + // err := logger.Sync() + // if err != nil && !errors.Is(err, syscall.ENOTTY) { + // zap.S().Errorf("日志同步失败 %+v", err) + // } + //}(logger) // 记录一条日志作为示例 logger.Debug("这是一个调试级别的日志") diff --git a/go-iot-mq/mq.go b/go-iot-mq/mq.go index ff263fd..044579c 100644 --- a/go-iot-mq/mq.go +++ b/go-iot-mq/mq.go @@ -18,7 +18,7 @@ var conn *amqp.Connection var chann *amqp.Channel func ConnectToRMQ() (err error) { - conn, err = amqp.Dial(rmqCredentials) + conn, err = amqp.Dial(genUrl(globalConfig.MQConfig)) if err != nil { return errors.New("Error de conexion: " + err.Error()) } diff --git a/go-iot/log.go b/go-iot/log.go index c847ce7..41564b1 100644 --- a/go-iot/log.go +++ b/go-iot/log.go @@ -1,11 +1,9 @@ package main import ( - "errors" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" - "syscall" "time" ) @@ -40,12 +38,12 @@ func InitLog() { zap.ReplaceGlobals(logger) // 替换全局 Logger // 确保日志被刷新 - defer func(logger *zap.Logger) { - err := logger.Sync() - if err != nil && !errors.Is(err, syscall.ENOTTY) { - zap.S().Errorf("日志同步失败 %+v", err) - } - }(logger) + //defer func(logger *zap.Logger) { + // err := logger.Sync() + // if err != nil && !errors.Is(err, syscall.ENOTTY) { + // zap.S().Errorf("日志同步失败 %+v", err) + // } + //}(logger) // 记录一条日志作为示例 logger.Debug("这是一个调试级别的日志") diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index 59031f6..01f4a51 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -2,7 +2,6 @@ package initialize import ( "context" - "errors" "flag" "fmt" "github.com/gin-gonic/gin" @@ -27,7 +26,6 @@ import ( "log" "net/url" "os" - "syscall" "time" ) @@ -60,8 +58,7 @@ var ( clickTransmitApi = transmit.ClickhouseTransmitApi{} cassandraTransmitApi = transmit.CassandraTransmitApi{} - - feishuApi = notice.FeiShuApi{} + feishuApi = notice.FeiShuApi{} dingdingApi = notice.DingDingApi{} ) @@ -408,12 +405,12 @@ func initLog() { zap.ReplaceGlobals(lg) // 替换全局 Logger // 确保日志被刷新 - defer func(lg *zap.Logger) { - err := lg.Sync() - if err != nil && !errors.Is(err, syscall.ENOTTY) { - zap.S().Errorf("日志同步失败 %+v", err) - } - }(lg) + //defer func(lg *zap.Logger) { + // err := lg.Sync() + // if err != nil && !errors.Is(err, syscall.ENOTTY) { + // zap.S().Errorf("日志同步失败 %+v", err) + // } + //}(lg) // 记录一条日志作为示例 lg.Debug("这是一个调试级别的日志") @@ -598,11 +595,6 @@ func initRouter(r *gin.RouterGroup) { r.GET("/MessageList/page", messageListApi.PageMessageList) - - - - - r.POST("/DingDing/create", dingdingApi.CreateDingDing) r.POST("/DingDing/update", dingdingApi.UpdateDingDing) r.GET("/DingDing/:id", dingdingApi.ByIdDingDing) @@ -610,8 +602,6 @@ func initRouter(r *gin.RouterGroup) { r.POST("/DingDing/delete/:id", dingdingApi.DeleteDingDing) r.POST("/DingDing/bind", dingdingApi.Bind) - - r.POST("/FeiShuId/create", feishuApi.CreateFeiShu) r.POST("/FeiShuId/update", feishuApi.UpdateFeiShu) r.GET("/FeiShuId/:id", feishuApi.ByIdFeiShu) -- Gitee From 6532a013daa748d9c647f7ca0f67d4a89d119cf5 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Fri, 19 Jul 2024 11:31:57 +0800 Subject: [PATCH 04/90] =?UTF-8?q?fix:start.sh=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/start.sh b/docker/start.sh index eb04a96..0184539 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,3 +1,3 @@ #!/bin/zsh - docker-compose -f env/base-env-docker-compose.yml up -f app/docker-compose.yml -d + docker-compose -f env/base-env-docker-compose.yml -f app/docker-compose.yml -d echo "执行完毕,项目启动中..." -- Gitee From 6e5cd090ef3d60f09b1cf69af24eeaa7211eb930 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Fri, 19 Jul 2024 11:41:34 +0800 Subject: [PATCH 05/90] =?UTF-8?q?fix:app=E7=9A=84docker-compose=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app/docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index b550331..64451cb 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -43,6 +43,8 @@ services: context: ../../ dockerfile: deploy/IotGoProject.Dockerfile image: go-iot-project:latest + volumes: + - ./iot-project/config/app-local.yml:/app/app-local.yml ports: - 8005:8080 networks: @@ -53,6 +55,8 @@ services: dockerfile: deploy/IotMQTT.Dockerfile image: go-iot-mqtt:latest entrypoint: ["/app/main", "-config", "/app/app-local.yml"] + volumes: + - ./mqtt/config/app-local.yml:/app/app-local.yml ports: - 8006:8081 networks: @@ -62,6 +66,8 @@ services: ports: - 8007:8081 entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] + volumes: + - ./mqtt/config/app-local2.yml:/app/app-local2.yml networks: - iot-net iotgomqtt3: @@ -69,6 +75,8 @@ services: ports: - 8008:8081 entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] + volumes: + - ./mqtt/config/app-local3.yml:/app/app-local3.yml networks: - iot-net networks: -- Gitee From 15d818384fbcbb6824f92b6c19e71c47df4271fe Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Mon, 22 Jul 2024 11:52:26 +0800 Subject: [PATCH 06/90] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9env=E7=9A=84docker?= =?UTF-8?q?-compose,app=E7=9A=84docker-compose=E6=96=87=E4=BB=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=89=8D=E7=AB=AF=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/.env.docker | 6 + ant-vue/nginx.conf | 109 +++++++++++++++++++ ant-vue/package.json | 1 + ant-vue/src/views/production-plans/index.vue | 6 + ant-vue/src/views/repair-records/index.vue | 7 ++ deploy/IotAdminVue.Dockerfile | 20 ++++ docker/app/docker-compose.yml | 67 +++++++----- docker/app/iot-project/config/app-local.yml | 4 +- docker/env/base-env-docker-compose.yml | 22 ++-- docker/env/mysql/docker-compose.yml | 4 +- docker/start.sh | 4 +- 11 files changed, 208 insertions(+), 42 deletions(-) create mode 100644 ant-vue/.env.docker create mode 100644 ant-vue/nginx.conf create mode 100644 deploy/IotAdminVue.Dockerfile diff --git a/ant-vue/.env.docker b/ant-vue/.env.docker new file mode 100644 index 0000000..aa811f8 --- /dev/null +++ b/ant-vue/.env.docker @@ -0,0 +1,6 @@ +NODE_ENV=docker +# 静态文件路径 +VITE_BASE_URL= +VITE_APP_ENV_NAME=线上环境 +VITE_APP_API_URL=http://192.168.3.110:8005 +VITE_LOGIN=some diff --git a/ant-vue/nginx.conf b/ant-vue/nginx.conf new file mode 100644 index 0000000..875771b --- /dev/null +++ b/ant-vue/nginx.conf @@ -0,0 +1,109 @@ + +user root; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + +error_log /var/log/nginx/error.log error; + + +events { + worker_connections 1024; +} + + +http { + include mime.types; + default_type application/octet-stream; + + #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + # '$status $body_bytes_sent "$http_referer" ' + # '"$http_user_agent" "$http_x_forwarded_for"'; + + #access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + #keepalive_timeout 0; + keepalive_timeout 65; + + #gzip on; + + log_format main escape=json + '{"timestamp":"$time_iso8601",' + '"host":"$hostname",' + '"server_ip":"$server_addr",' + '"client_ip":"$remote_addr",' + '"xff":"$http_x_forwarded_for",' + '"domain":"$host",' + '"url":"$uri",' + '"referer":"$http_referer",' + '"args":"$args",' + '"upstreamtime":"$upstream_response_time",' + '"responsetime":"$request_time",' + '"request_method":"$request_method",' + '"request_body":"$request_body",' + '"status":"$status",' + '"size":"$body_bytes_sent",' + '"request_length":"$request_length",' + '"protocol":"$server_protocol",' + '"upstreamhost":"$upstream_addr",' + '"http_user_agent":"$http_user_agent",' + '"http_token":"$http_token"' + '}'; + + access_log /var/log/nginx/access.log main; + + gzip on; + gzip_vary on; + gzip_disable "MSIE [1-6]\."; + gzip_proxied any; + gzip_min_length 1024; + gzip_comp_level 4; + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + #客户端请求头部的缓冲区大小,这个可以根据系统分页大小来设置, + #由于一般系统分页都要大于1k,所以这里设置为系统分页大小。查看系统分页可以使用 getconf PAGESIZE命令 + client_header_buffer_size 64k; + large_client_header_buffers 4 64k; + #为打开文件指定缓存,默认是没有启用的,max指定缓存最大数量,建议和打开文件数一致, + #inactive是指经过多长时间文件没被请求后删除缓存 打开文件最大数量为我们再main配置的worker_rlimit_nofile参数 + open_file_cache max=2000 inactive=60s; + #这个是指多长时间检查一次缓存的有效信息。如果有一个文件在inactive时间内一次没被使用,它将被移除 + open_file_cache_valid 60s; + #open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字, + #文件描述符一直是在缓存中打开的,如果有一个文件在inactive时间内一次没被使用,它将被移除。 + open_file_cache_min_uses 5; + + #代理 请求头设置 + proxy_set_header Host $host; # 全局上会导致代理域名有误 代理域名需要单独配置 proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_intercept_errors on; + recursive_error_pages on; + +# include ./conf.d/*.conf; +# include ./conf.d/**/*.conf; + + server { + listen 80; + + access_log /var/log/nginx/iot/80.access.log main; + error_log /var/log/nginx/iot/80.error.log ; + + error_page 500 502 503 504 /50x.html; + + location / { + root /app/iot/project/html; + try_files $uri $uri/ /index.html; + } + + } + +} diff --git a/ant-vue/package.json b/ant-vue/package.json index bd5238b..2dac31c 100644 --- a/ant-vue/package.json +++ b/ant-vue/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "build-docker": "vite build --mode docker", "lint": "eslint . --ext .cjs,.ts,.vue --fix", "svgo:simple": "svgo -f src/icons/simple --config=src/icons/simple.config.cjs", "svgo:complex": "svgo -f src/icons/complex --config=src/icons/complex.config.cjs" diff --git a/ant-vue/src/views/production-plans/index.vue b/ant-vue/src/views/production-plans/index.vue index e69de29..d99bac3 100644 --- a/ant-vue/src/views/production-plans/index.vue +++ b/ant-vue/src/views/production-plans/index.vue @@ -0,0 +1,6 @@ + + + + diff --git a/ant-vue/src/views/repair-records/index.vue b/ant-vue/src/views/repair-records/index.vue index e69de29..a182dcc 100644 --- a/ant-vue/src/views/repair-records/index.vue +++ b/ant-vue/src/views/repair-records/index.vue @@ -0,0 +1,7 @@ + + + + diff --git a/deploy/IotAdminVue.Dockerfile b/deploy/IotAdminVue.Dockerfile new file mode 100644 index 0000000..5b10f1b --- /dev/null +++ b/deploy/IotAdminVue.Dockerfile @@ -0,0 +1,20 @@ +FROM node:18.19.0-alpine3.18 as build + + +WORKDIR /app +COPY ../ant-vue ./ant-vue + +RUN cd ant-vue && npm install --registry=https://registry.npmmirror.com && npm run build-docker + + + +FROM nginx:stable-alpine3.17 + + +COPY ../ant-vue/nginx.conf /etc/nginx/nginx.conf +WORKDIR /app +COPY --from=build /app/ant-vue/dist /app/iot/project/html + +RUN mkdir /var/log/nginx/iot + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index 64451cb..007578e 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -1,4 +1,34 @@ services: + iotgomqtt1: + build: + context: ../../ + dockerfile: deploy/IotMQTT.Dockerfile + image: go-iot-mqtt:latest + entrypoint: ["/app/main", "-config", "/app/app-local.yml"] + volumes: + - ./mqtt/config/app-local.yml:/app/app-local.yml + ports: + - 8006:8081 + networks: + - iot-net + iotgomqtt2: + image: go-iot-mqtt:latest + ports: + - 8007:8081 + entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] + volumes: + - ./mqtt/config/app-local2.yml:/app/app-local2.yml + networks: + - iot-net + iotgomqtt3: + image: go-iot-mqtt:latest + ports: + - 8008:8081 + entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] + volumes: + - ./mqtt/config/app-local3.yml:/app/app-local3.yml + networks: + - iot-net iotgomq-pre_handler: build: context: ../../ @@ -9,6 +39,8 @@ services: volumes: - ./mq/config/app-local-pre_handler.yml:/app/app-local-pre_handler.yml entrypoint: ["/app/main", "-config", "/app/app-local-pre_handler.yml"] + depends_on: + - iotgomqtt1 networks: - iot-net iotgomq-calc_handler: @@ -18,6 +50,8 @@ services: volumes: - ./mq/config/app-local-calc.yml:/app/app-local-calc.yml entrypoint: ["/app/main", "-config", "/app/app-local-calc.yml"] + depends_on: + - iotgomq-pre_handler networks: - iot-net iotgomq-waring_handler: @@ -27,6 +61,8 @@ services: volumes: - ./mq/config/app-local-waring_handler.yml:/app/app-local-waring_handler.yml entrypoint: ["/app/main", "-config", "/app/app-local-waring_handler.yml"] + depends_on: + - iotgomq-calc_handler networks: - iot-net iotgomq-wd_handler: @@ -36,6 +72,8 @@ services: volumes: - ./mq/config/app-local-wd.yml:/app/app-local-wd.yml entrypoint: ["/app/main", "-config", "/app/app-local-wd.yml"] + depends_on: + - iotgomq-calc_handler networks: - iot-net iotgoproject: @@ -49,34 +87,13 @@ services: - 8005:8080 networks: - iot-net - iotgomqtt1: + iot-admin-vue: build: context: ../../ - dockerfile: deploy/IotMQTT.Dockerfile - image: go-iot-mqtt:latest - entrypoint: ["/app/main", "-config", "/app/app-local.yml"] - volumes: - - ./mqtt/config/app-local.yml:/app/app-local.yml - ports: - - 8006:8081 - networks: - - iot-net - iotgomqtt2: - image: go-iot-mqtt:latest + dockerfile: deploy/IotAdminVue.Dockerfile + image: go-iot-admin-vue:latest ports: - - 8007:8081 - entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] - volumes: - - ./mqtt/config/app-local2.yml:/app/app-local2.yml - networks: - - iot-net - iotgomqtt3: - image: go-iot-mqtt:latest - ports: - - 8008:8081 - entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] - volumes: - - ./mqtt/config/app-local3.yml:/app/app-local3.yml + - 8080:80 networks: - iot-net networks: diff --git a/docker/app/iot-project/config/app-local.yml b/docker/app/iot-project/config/app-local.yml index 212246e..247e3d9 100644 --- a/docker/app/iot-project/config/app-local.yml +++ b/docker/app/iot-project/config/app-local.yml @@ -19,8 +19,8 @@ influx_config: bucket: buc mysql_config: - username: root - password: root123@ + username: app + password: iot123456 host: 192.168.7.41 port: 3306 dbname: iot diff --git a/docker/env/base-env-docker-compose.yml b/docker/env/base-env-docker-compose.yml index 25b90ae..dfcbef8 100644 --- a/docker/env/base-env-docker-compose.yml +++ b/docker/env/base-env-docker-compose.yml @@ -4,7 +4,7 @@ services: influxdb: image: influxdb:2.6-alpine env_file: - - influxv2.env + - ./influx/influxv2.env volumes: # Mount for influxdb data directory and configuration - influxdbv2:/var/lib/influxdb2:rw @@ -13,12 +13,12 @@ services: networks: - iot-net telegraf: - image: telegraf:1.25-alpine + image: telegraf:1.25 depends_on: - influxdb volumes: # Mount for telegraf config - - influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro + - ./influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro env_file: - influx/influxv2.env networks: @@ -29,7 +29,7 @@ services: ports: - 27017:27017 volumes: - - ./database:/data/db + - ./mongo/database:/data/db environment: - MONGO_INITDB_ROOT_USERNAME=admin - MONGO_INITDB_ROOT_PASSWORD=admin @@ -75,12 +75,12 @@ services: restart: always environment: MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: mysql + MYSQL_DATABASE: iot MYSQL_USER: app MYSQL_PASSWORD: iot123456 TZ: "Asia/Shanghai" volumes: - - mysql/data:/var/lib/mysql + - ./mysql/data:/var/lib/mysql ports: - "3306:3306" command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --binlog-format=ROW @@ -93,10 +93,10 @@ services: - 5672:5672 - 15672:15672 volumes: - - rabbitmq/data/:/var/lib/rabbitmq/ - - rabbitmq/log/:/var/log/rabbitmq - - rabbitmq/plugins:/usr/lib/rabbitmq/plugins - - rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw + - ./rabbitmq/data/:/var/lib/rabbitmq/ + - ./rabbitmq/log/:/var/log/rabbitmq + - ./rabbitmq/plugins:/usr/lib/rabbitmq/plugins + - ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw environment: - RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/usr/lib/rabbitmq/plugins networks: @@ -109,7 +109,7 @@ services: - '6379:6379' command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 volumes: - - redis/data:/data + - ./redis/data:/data networks: - iot-net volumes: diff --git a/docker/env/mysql/docker-compose.yml b/docker/env/mysql/docker-compose.yml index b281053..275ff8e 100644 --- a/docker/env/mysql/docker-compose.yml +++ b/docker/env/mysql/docker-compose.yml @@ -2,12 +2,12 @@ version: '3' services: mysql: - image: mysql:8.0 + image: mysql:8.0.38 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: mysql + MYSQL_DATABASE: iot MYSQL_USER: app MYSQL_PASSWORD: iot123456 TZ: "Asia/Shanghai" diff --git a/docker/start.sh b/docker/start.sh index 0184539..1d9108d 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,3 +1,3 @@ #!/bin/zsh - docker-compose -f env/base-env-docker-compose.yml -f app/docker-compose.yml -d - echo "执行完毕,项目启动中..." +docker compose -f ./env/base-env-docker-compose.yml -f ./app/docker-compose.yml up -d +echo "执行完毕,项目启动中..." -- Gitee From 386b196fc942babf035939e86e910658aa4127a2 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Mon, 22 Jul 2024 12:41:04 +0800 Subject: [PATCH 07/90] feat: kafka + rabbit --- .../biz/transmit/kafka_transmit_biz.go | 73 +++++++ .../biz/transmit/rabbit_transmit_biz.go | 74 +++++++ iot-go-project/models/transmit.go | 5 +- .../router/transmit/kafka_transmit_router.go | 189 ++++++++++++++++++ .../router/transmit/rabbit_transmit_router.go | 189 ++++++++++++++++++ transmit/cache/transmit_cache.go | 17 ++ 6 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 iot-go-project/biz/transmit/kafka_transmit_biz.go create mode 100644 iot-go-project/biz/transmit/rabbit_transmit_biz.go create mode 100644 iot-go-project/router/transmit/kafka_transmit_router.go create mode 100644 iot-go-project/router/transmit/rabbit_transmit_router.go diff --git a/iot-go-project/biz/transmit/kafka_transmit_biz.go b/iot-go-project/biz/transmit/kafka_transmit_biz.go new file mode 100644 index 0000000..066debc --- /dev/null +++ b/iot-go-project/biz/transmit/kafka_transmit_biz.go @@ -0,0 +1,73 @@ +package transmit + +import ( + "context" + "encoding/json" + "igp/glob" + "igp/models" + "igp/servlet" + "iot-transmit/cache" + "strconv" +) + +type KafkaTransmitBiz struct{} + +func (biz *KafkaTransmitBiz) PageData(name string, page, size int) (*servlet.PaginationQ, error) { + var pagination servlet.PaginationQ + var KafkaTransmits []models.KafkaTransmit + + db := glob.GDb + + if name != "" { + db = db.Where("name like ?", "%"+name+"%") + } + + db.Model(&models.KafkaTransmit{}).Count(&pagination.Total) // 计算总记录数 + offset := (page - 1) * size + db.Offset(offset).Limit(size).Find(&KafkaTransmits) + + pagination.Data = KafkaTransmits + pagination.Page = page + pagination.Size = size + + return &pagination, nil +} + +func (biz *KafkaTransmitBiz) Bind(req models.KafkaTransmitBind) { + glob.GDb.Model(models.KafkaTransmitBind{}).Create(req) + jsonData := biz.toByte(req) + // 缓存构造 + glob.GRedis.LPush(context.Background(), "transmit:Kafka:"+strconv.Itoa(req.MqttClientId), jsonData) +} + +func (biz *KafkaTransmitBiz) toByte(req models.KafkaTransmitBind) []byte { + var KafkaInfo models.KafkaTransmit + + glob.GDb.First(&KafkaInfo, req.KafkaTransmitId) + + v := cache.KafkaTransmitCache{ + ID: "Kafka-" + strconv.Itoa(int(req.ID)), + Host: KafkaInfo.Host, + Port: KafkaInfo.Port, + Script: req.Script, + Topic: req.Topic, + } + jsonData, err := json.Marshal(v) + if err != nil { + panic(err) + } + return jsonData +} + +// ChangeEnable 修改启用状态 +func (biz *KafkaTransmitBiz) ChangeEnable(req models.KafkaTransmitBind) { + glob.GDb.Model(models.KafkaTransmitBind{}).Where("id = ?", req.ID).Update("enable", req.Enable) + glob.GRedis.LRem(context.Background(), "transmit:Kafka:"+strconv.Itoa(req.MqttClientId), 1, biz.toByte(req)) +} + +//var KafkaOp = Kafka.KafkaOp{} +// +//// MockScript 模拟执行脚本 +//func (biz *KafkaTransmitBiz) MockScript(dataRowList []common.DataRowList, script string) [][]Kafka.KafkaParam { +// return KafkaOp.RunScript(dataRowList, script) +//} diff --git a/iot-go-project/biz/transmit/rabbit_transmit_biz.go b/iot-go-project/biz/transmit/rabbit_transmit_biz.go new file mode 100644 index 0000000..b887f95 --- /dev/null +++ b/iot-go-project/biz/transmit/rabbit_transmit_biz.go @@ -0,0 +1,74 @@ +package transmit + +import ( + "context" + "encoding/json" + "igp/glob" + "igp/models" + "igp/servlet" + "iot-transmit/cache" + "strconv" +) + +type RabbitTransmitBiz struct{} + +func (biz *RabbitTransmitBiz) PageData(name string, page, size int) (*servlet.PaginationQ, error) { + var pagination servlet.PaginationQ + var RabbitTransmits []models.RabbitmqTransmit + + db := glob.GDb + + if name != "" { + db = db.Where("name like ?", "%"+name+"%") + } + + db.Model(&models.RabbitmqTransmit{}).Count(&pagination.Total) // 计算总记录数 + offset := (page - 1) * size + db.Offset(offset).Limit(size).Find(&RabbitTransmits) + + pagination.Data = RabbitTransmits + pagination.Page = page + pagination.Size = size + + return &pagination, nil +} + +func (biz *RabbitTransmitBiz) Bind(req models.RabbitmqTransmitBind) { + glob.GDb.Model(models.RabbitmqTransmitBind{}).Create(req) + jsonData := biz.toByte(req) + // 缓存构造 + glob.GRedis.LPush(context.Background(), "transmit:Rabbit:"+strconv.Itoa(req.MqttClientId), jsonData) +} + +func (biz *RabbitTransmitBiz) toByte(req models.RabbitmqTransmitBind) []byte { + var RabbitInfo models.RabbitmqTransmit + + glob.GDb.First(&RabbitInfo, req.RabbitmqTransmitId) + + v := cache.RabbitTransmitCache{ + ID: "Rabbit-" + strconv.Itoa(int(req.ID)), + Host: RabbitInfo.Host, + Port: RabbitInfo.Port, + Script: req.Script, + Exchange: req.Exchange, + RoutingKey: req.RoutingKey, + } + jsonData, err := json.Marshal(v) + if err != nil { + panic(err) + } + return jsonData +} + +// ChangeEnable 修改启用状态 +func (biz *RabbitTransmitBiz) ChangeEnable(req models.RabbitmqTransmitBind) { + glob.GDb.Model(models.RabbitmqTransmitBind{}).Where("id = ?", req.ID).Update("enable", req.Enable) + glob.GRedis.LRem(context.Background(), "transmit:Rabbit:"+strconv.Itoa(req.MqttClientId), 1, biz.toByte(req)) +} + +//var RabbitOp = Rabbit.RabbitOp{} +// +//// MockScript 模拟执行脚本 +//func (biz *RabbitTransmitBiz) MockScript(dataRowList []common.DataRowList, script string) [][]Rabbit.RabbitParam { +// return RabbitOp.RunScript(dataRowList, script) +//} diff --git a/iot-go-project/models/transmit.go b/iot-go-project/models/transmit.go index edce10a..23260b9 100644 --- a/iot-go-project/models/transmit.go +++ b/iot-go-project/models/transmit.go @@ -129,11 +129,14 @@ type KafkaTransmit struct { Host string `json:"host" gorm:"column:host;type:varchar(255);"` Port int `json:"port" gorm:"column:port;type:int(10);"` } -type KafakaTransmitBind struct { +type KafkaTransmitBind struct { gorm.Model `structs:"-"` MqttClientId int `json:"mqtt_client_id"` // MQTT客户端表的外键ID KafkaTransmitId uint `json:"kafka_transmit_id" gorm:"column:kafka_transmit_id;type:int(10);"` // 传输表 Topic string `json:"topic" gorm:"column:topic;type:varchar(255);"` // topic + Script string `json:"script" gorm:"column:script"` // 转换insert语句的脚本 + Enable bool `json:"enable" gorm:"column:enable;type:tinyint(1);" ` // 是否启用 + } diff --git a/iot-go-project/router/transmit/kafka_transmit_router.go b/iot-go-project/router/transmit/kafka_transmit_router.go new file mode 100644 index 0000000..acdb854 --- /dev/null +++ b/iot-go-project/router/transmit/kafka_transmit_router.go @@ -0,0 +1,189 @@ +package transmit + +import ( + "github.com/gin-gonic/gin" + "igp/biz/transmit" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type KafkaTransmitApi struct{} + +var KafkaTransmit = transmit.KafkaTransmitBiz{} + +// CreateKafkaTransmit +// @Summary 创建Kafka数据库管理 +// @Description 创建Kafka数据库管理 +// @Tags KafkaTransmits +// @Accept json +// @Produce json +// @Param KafkaTransmit body models.KafkaTransmit true "Kafka数据库管理" +// @Success 201 {object} servlet.JSONResult{data=models.KafkaTransmit} "创建成功的Kafka数据库管理" +// @Failure 400 {string} string "请求数据错误" +// @Failure 500 {string} string "内部服务器错误" +// @Router /KafkaTransmit/create [post] +func (api *KafkaTransmitApi) CreateKafkaTransmit(c *gin.Context) { + var KafkaTransmit models.KafkaTransmit + if err := c.ShouldBindJSON(&KafkaTransmit); err != nil { + servlet.Error(c, err.Error()) + return + } + + // 检查 KafkaTransmit 是否被正确初始化 + if KafkaTransmit.Name == "" { + servlet.Error(c, "名称不能为空") + return + } + + result := glob.GDb.Create(&KafkaTransmit) + + if result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + // 返回创建成功的Kafka数据库管理 + servlet.Resp(c, KafkaTransmit) +} + +// UpdateKafkaTransmit +// @Summary 更新一个Kafka数据库管理 +// @Description 更新一个Kafka数据库管理 +// @Tags KafkaTransmits +// @Accept json +// @Produce json +// @Param KafkaTransmit body models.KafkaTransmit true "Kafka数据库管理" +// @Success 200 {object} servlet.JSONResult{data=models.KafkaTransmit} "Kafka数据库管理" +// @Failure 400 {string} string "请求数据错误" +// @Failure 404 {string} string "Kafka数据库管理未找到" +// @Failure 500 {string} string "内部服务器错误" +// @Router /KafkaTransmit/update [post] +func (api *KafkaTransmitApi) UpdateKafkaTransmit(c *gin.Context) { + var req models.KafkaTransmit + if err := c.ShouldBindJSON(&req); err != nil { + + servlet.Error(c, err.Error()) + return + } + + var old models.KafkaTransmit + result := glob.GDb.First(&old, req.ID) + if result.Error != nil { + + servlet.Error(c, "KafkaTransmit not found") + return + } + + var newV models.KafkaTransmit + newV = old + newV.Name = req.Name + result = glob.GDb.Model(&newV).Updates(newV) + + if result.Error != nil { + + servlet.Error(c, result.Error.Error()) + return + } + servlet.Resp(c, old) +} + +// PageKafkaTransmit +// @Summary 分页查询Kafka数据库管理 +// @Description 分页查询Kafka数据库管理 +// @Tags KafkaTransmits +// @Accept json +// @Produce json +// @Param page query int false "页码" default(0) +// @Param page_size query int false "每页大小" default(10) +// @Success 200 {object} servlet.JSONResult{data=servlet.PaginationQ{data=models.KafkaTransmit}} "Kafka数据库管理" +// @Failure 400 {string} string "请求参数错误" +// @Failure 500 {string} string "查询异常" +// @Router /KafkaTransmit/page [get] +func (api *KafkaTransmitApi) PageKafkaTransmit(c *gin.Context) { + var name = c.Query("name") + var page = c.DefaultQuery("page", "0") + var pageSize = c.DefaultQuery("page_size", "10") + parseUint, err := strconv.Atoi(page) + if err != nil { + servlet.Error(c, "无效的页码") + return + } + u, err := strconv.Atoi(pageSize) + + if err != nil { + servlet.Error(c, "无效的页长") + return + } + + data, err := KafkaTransmit.PageData(name, parseUint, u) + if err != nil { + servlet.Error(c, "查询异常") + return + } + servlet.Resp(c, data) +} + +// DeleteKafkaTransmit +// @Tags KafkaTransmits +// @Summary 删除Kafka数据库管理 +// @Produce application/json +// @Param id path int true "主键" +// @Router /KafkaTransmit/delete/:id [post] +func (api *KafkaTransmitApi) DeleteKafkaTransmit(c *gin.Context) { + var KafkaTransmit models.KafkaTransmit + + param := c.Param("id") + + result := glob.GDb.First(&KafkaTransmit, param) + if result.Error != nil { + servlet.Error(c, "KafkaTransmit not found") + + return + } + + if result := glob.GDb.Delete(&KafkaTransmit); result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + + servlet.Resp(c, "删除成功") +} + +// ByIdKafkaTransmit +// @Tags KafkaTransmits +// @Summary 单个详情 +// @Param id path int true "主键" +// @Produce application/json +// @Router /KafkaTransmit/:id [get] +func (api *KafkaTransmitApi) ByIdKafkaTransmit(c *gin.Context) { + var KafkaTransmit models.KafkaTransmit + + param := c.Param("id") + + result := glob.GDb.First(&KafkaTransmit, param) + if result.Error != nil { + servlet.Error(c, "KafkaTransmit not found") + + return + } + + servlet.Resp(c, KafkaTransmit) +} + +//// MockScript +//// @Tags KafkaTransmits +//// @Summary 模拟脚本 +//// @Param KafkaTransmit body servlet.TransmitScriptParam true "执行参数" +//// @Produce application/json +//// @Router /KafkaTransmit/mockScript [post] +//func (api *KafkaTransmitApi) MockScript(c *gin.Context) { +// var req servlet.TransmitScriptParam +// if err := c.ShouldBindJSON(&req); err != nil { +// +// servlet.Error(c, err.Error()) +// return +// } +// script := KafkaTransmit.MockScript(req.DataRowList, req.Script) +// servlet.Resp(c, script) +//} diff --git a/iot-go-project/router/transmit/rabbit_transmit_router.go b/iot-go-project/router/transmit/rabbit_transmit_router.go new file mode 100644 index 0000000..05c64a9 --- /dev/null +++ b/iot-go-project/router/transmit/rabbit_transmit_router.go @@ -0,0 +1,189 @@ +package transmit + +import ( + "github.com/gin-gonic/gin" + "igp/biz/transmit" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type RabbitmqTransmitApi struct{} + +var RabbitmqTransmit = transmit.RabbitTransmitBiz{} + +// CreateRabbitmqTransmit +// @Summary 创建Rabbit消息队列管理 +// @Description 创建Rabbit消息队列管理 +// @Tags RabbitmqTransmits +// @Accept json +// @Produce json +// @Param RabbitmqTransmit body models.RabbitmqTransmit true "Rabbit消息队列管理" +// @Success 201 {object} servlet.JSONResult{data=models.RabbitmqTransmit} "创建成功的Rabbit消息队列管理" +// @Failure 400 {string} string "请求数据错误" +// @Failure 500 {string} string "内部服务器错误" +// @Router /RabbitmqTransmit/create [post] +func (api *RabbitmqTransmitApi) CreateRabbitmqTransmit(c *gin.Context) { + var RabbitmqTransmit models.RabbitmqTransmit + if err := c.ShouldBindJSON(&RabbitmqTransmit); err != nil { + servlet.Error(c, err.Error()) + return + } + + // 检查 RabbitmqTransmit 是否被正确初始化 + if RabbitmqTransmit.Name == "" { + servlet.Error(c, "名称不能为空") + return + } + + result := glob.GDb.Create(&RabbitmqTransmit) + + if result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + // 返回创建成功的Rabbit消息队列管理 + servlet.Resp(c, RabbitmqTransmit) +} + +// UpdateRabbitmqTransmit +// @Summary 更新一个Rabbit消息队列管理 +// @Description 更新一个Rabbit消息队列管理 +// @Tags RabbitmqTransmits +// @Accept json +// @Produce json +// @Param RabbitmqTransmit body models.RabbitmqTransmit true "Rabbit消息队列管理" +// @Success 200 {object} servlet.JSONResult{data=models.RabbitmqTransmit} "Rabbit消息队列管理" +// @Failure 400 {string} string "请求数据错误" +// @Failure 404 {string} string "Rabbit消息队列管理未找到" +// @Failure 500 {string} string "内部服务器错误" +// @Router /RabbitmqTransmit/update [post] +func (api *RabbitmqTransmitApi) UpdateRabbitmqTransmit(c *gin.Context) { + var req models.RabbitmqTransmit + if err := c.ShouldBindJSON(&req); err != nil { + + servlet.Error(c, err.Error()) + return + } + + var old models.RabbitmqTransmit + result := glob.GDb.First(&old, req.ID) + if result.Error != nil { + + servlet.Error(c, "RabbitmqTransmit not found") + return + } + + var newV models.RabbitmqTransmit + newV = old + newV.Name = req.Name + result = glob.GDb.Model(&newV).Updates(newV) + + if result.Error != nil { + + servlet.Error(c, result.Error.Error()) + return + } + servlet.Resp(c, old) +} + +// PageRabbitmqTransmit +// @Summary 分页查询Rabbit消息队列管理 +// @Description 分页查询Rabbit消息队列管理 +// @Tags RabbitmqTransmits +// @Accept json +// @Produce json +// @Param page query int false "页码" default(0) +// @Param page_size query int false "每页大小" default(10) +// @Success 200 {object} servlet.JSONResult{data=servlet.PaginationQ{data=models.RabbitmqTransmit}} "Rabbit消息队列管理" +// @Failure 400 {string} string "请求参数错误" +// @Failure 500 {string} string "查询异常" +// @Router /RabbitmqTransmit/page [get] +func (api *RabbitmqTransmitApi) PageRabbitmqTransmit(c *gin.Context) { + var name = c.Query("name") + var page = c.DefaultQuery("page", "0") + var pageSize = c.DefaultQuery("page_size", "10") + parseUint, err := strconv.Atoi(page) + if err != nil { + servlet.Error(c, "无效的页码") + return + } + u, err := strconv.Atoi(pageSize) + + if err != nil { + servlet.Error(c, "无效的页长") + return + } + + data, err := RabbitmqTransmit.PageData(name, parseUint, u) + if err != nil { + servlet.Error(c, "查询异常") + return + } + servlet.Resp(c, data) +} + +// DeleteRabbitmqTransmit +// @Tags RabbitmqTransmits +// @Summary 删除Rabbit消息队列管理 +// @Produce application/json +// @Param id path int true "主键" +// @Router /RabbitmqTransmit/delete/:id [post] +func (api *RabbitmqTransmitApi) DeleteRabbitmqTransmit(c *gin.Context) { + var RabbitmqTransmit models.RabbitmqTransmit + + param := c.Param("id") + + result := glob.GDb.First(&RabbitmqTransmit, param) + if result.Error != nil { + servlet.Error(c, "RabbitmqTransmit not found") + + return + } + + if result := glob.GDb.Delete(&RabbitmqTransmit); result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + + servlet.Resp(c, "删除成功") +} + +// ByIdRabbitmqTransmit +// @Tags RabbitmqTransmits +// @Summary 单个详情 +// @Param id path int true "主键" +// @Produce application/json +// @Router /RabbitmqTransmit/:id [get] +func (api *RabbitmqTransmitApi) ByIdRabbitmqTransmit(c *gin.Context) { + var RabbitmqTransmit models.RabbitmqTransmit + + param := c.Param("id") + + result := glob.GDb.First(&RabbitmqTransmit, param) + if result.Error != nil { + servlet.Error(c, "RabbitmqTransmit not found") + + return + } + + servlet.Resp(c, RabbitmqTransmit) +} + +//// MockScript +//// @Tags RabbitmqTransmits +//// @Summary 模拟脚本 +//// @Param RabbitmqTransmit body servlet.TransmitScriptParam true "执行参数" +//// @Produce application/json +//// @Router /RabbitmqTransmit/mockScript [post] +//func (api *RabbitmqTransmitApi) MockScript(c *gin.Context) { +// var req servlet.TransmitScriptParam +// if err := c.ShouldBindJSON(&req); err != nil { +// +// servlet.Error(c, err.Error()) +// return +// } +// script := RabbitmqTransmit.MockScript(req.DataRowList, req.Script) +// servlet.Resp(c, script) +//} diff --git a/transmit/cache/transmit_cache.go b/transmit/cache/transmit_cache.go index db855ea..deb5a06 100644 --- a/transmit/cache/transmit_cache.go +++ b/transmit/cache/transmit_cache.go @@ -54,3 +54,20 @@ type CassandraTransmitCache struct { Table string `json:"table"` Script string `json:"script"` } + +type KafkaTransmitCache struct { + ID string `json:"ID"` + Host string `json:"host"` + Port int `json:"port" ` + Topic string `json:"topic"` + Script string `json:"script"` +} +type RabbitTransmitCache struct { + ID string `json:"ID"` + Host string `json:"host"` + Port int `json:"port" ` + Script string `json:"script"` + Exchange string `json:"exchange" ` // 交换机 + RoutingKey string `json:"routing_key" ` // 路由键 + +} -- Gitee From e98f9e0ef0fb52ee1c029912b60d5d1c74ef413a Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Mon, 22 Jul 2024 13:16:57 +0800 Subject: [PATCH 08/90] feat: modbus --- modbus/client.go | 28 ++++++++++++++++++++++++++++ modbus/go.mod | 10 ++++++++++ modbus/go.sum | 6 ++++++ modbus/server.go | 23 +++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 modbus/client.go create mode 100644 modbus/go.mod create mode 100644 modbus/go.sum create mode 100644 modbus/server.go diff --git a/modbus/client.go b/modbus/client.go new file mode 100644 index 0000000..3b873fb --- /dev/null +++ b/modbus/client.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/goburrow/modbus" +) + +func client() { + handler := modbus.NewTCPClientHandler("localhost:1502") + // Connect manually so that multiple requests are handled in one session + err := handler.Connect() + defer handler.Close() + client := modbus.NewClient(handler) + + + + _, err = client.WriteMultipleRegisters(0, 5, []byte{0, 3, 0, 4, 0, 5,1,1,1,2}) + if err != nil { + fmt.Printf("%v\n", err) + } + + results, err := client.ReadHoldingRegisters(0, 5) + if err != nil { + fmt.Printf("%v\n", err) + } + fmt.Printf("results %v\n", results) +} diff --git a/modbus/go.mod b/modbus/go.mod new file mode 100644 index 0000000..488eebe --- /dev/null +++ b/modbus/go.mod @@ -0,0 +1,10 @@ +module iot-modbus + +go 1.22 + +require github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2 + +require ( + github.com/goburrow/modbus v0.1.0 // indirect + github.com/goburrow/serial v0.1.0 // indirect +) diff --git a/modbus/go.sum b/modbus/go.sum new file mode 100644 index 0000000..3caf70b --- /dev/null +++ b/modbus/go.sum @@ -0,0 +1,6 @@ +github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro= +github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg= +github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= +github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= +github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2 h1:2H0HcvMX8JEa4HD32KJNBMwOBmCLs9xYOWVE8ig06Ss= +github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2/go.mod h1:qUzPVlSj2UgxJkVbH0ZwuuiR46U8RBMDT5KLY78Ifpw= diff --git a/modbus/server.go b/modbus/server.go new file mode 100644 index 0000000..3042504 --- /dev/null +++ b/modbus/server.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "time" + + "github.com/tbrandon/mbserver" +) + +func main() { + serv := mbserver.NewServer() + err := serv.ListenTCP("127.0.0.1:1502") + if err != nil { + log.Printf("%v\n", err) + } + defer serv.Close() + + // Wait forever + for { + time.Sleep(1 * time.Second) + client() + } +} \ No newline at end of file -- Gitee From c37afae355d18b51014d6ea7c5a92deb1ed40e03 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Mon, 22 Jul 2024 16:31:27 +0800 Subject: [PATCH 09/90] =?UTF-8?q?test:=20=E5=8D=8F=E8=AE=AE=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- protocol/coap/go.mod | 5 ++ protocol/coap/go.sum | 2 + protocol/coap/sample/client/main.go | 37 +++++++++ protocol/coap/sample/server/main.go | 59 ++++++++++++++ protocol/http/go.mod | 33 ++++++++ protocol/http/go.sum | 79 +++++++++++++++++++ protocol/http/main.go | 20 +++++ {modbus => protocol/modbus}/go.mod | 0 {modbus => protocol/modbus}/go.sum | 0 .../modbus/sample/client}/client.go | 7 +- .../modbus/sample/server}/server.go | 1 - protocol/readme.md | 6 ++ 12 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 protocol/coap/go.mod create mode 100644 protocol/coap/go.sum create mode 100644 protocol/coap/sample/client/main.go create mode 100644 protocol/coap/sample/server/main.go create mode 100644 protocol/http/go.mod create mode 100644 protocol/http/go.sum create mode 100644 protocol/http/main.go rename {modbus => protocol/modbus}/go.mod (100%) rename {modbus => protocol/modbus}/go.sum (100%) rename {modbus => protocol/modbus/sample/client}/client.go (93%) rename {modbus => protocol/modbus/sample/server}/server.go (96%) create mode 100644 protocol/readme.md diff --git a/protocol/coap/go.mod b/protocol/coap/go.mod new file mode 100644 index 0000000..6e45f02 --- /dev/null +++ b/protocol/coap/go.mod @@ -0,0 +1,5 @@ +module iot-coap + +go 1.22.4 + +require github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e diff --git a/protocol/coap/go.sum b/protocol/coap/go.sum new file mode 100644 index 0000000..0250cb3 --- /dev/null +++ b/protocol/coap/go.sum @@ -0,0 +1,2 @@ +github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e h1:oppjHFVTardH+VyOD32F9uBtgT5Wd/qVqEGcwj389Lc= +github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e/go.mod h1:as2rZ2aojRzZF8bGx1bPAn1yi9ICG6LwkiPOj6PBtjc= diff --git a/protocol/coap/sample/client/main.go b/protocol/coap/sample/client/main.go new file mode 100644 index 0000000..686606b --- /dev/null +++ b/protocol/coap/sample/client/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/dustin/go-coap" + "log" +) + +func main() { + + req := coap.Message{ + Type: coap.Confirmable, + Code: coap.GET, + MessageID: 12345, + Payload: []byte("hello, world!"), + } + + path := "/a" + + req.SetOption(coap.ETag, "weetag") + req.SetOption(coap.MaxAge, 3) + req.SetPathString(path) + + c, err := coap.Dial("udp", "localhost:5683") + if err != nil { + log.Fatalf("Error dialing: %v", err) + } + + rv, err := c.Send(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + + if rv != nil { + log.Printf("Response payload: %s", rv.Payload) + } + +} \ No newline at end of file diff --git a/protocol/coap/sample/server/main.go b/protocol/coap/sample/server/main.go new file mode 100644 index 0000000..a86c94b --- /dev/null +++ b/protocol/coap/sample/server/main.go @@ -0,0 +1,59 @@ +package main + +import ( //导入相应的包 + + "github.com/dustin/go-coap" + "log" + "net" +) + +func handleA(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message { + log.Printf("Got message in handleA: path=%q: %#v from %v", m.Path(), m, a) + + log.Printf("Message = %s" , string(m.Payload)) + + if m.IsConfirmable() { + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("hello to you!"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + + + log.Printf("Transmitting from A %#v", res) + return res + } + return nil +} + +func handleB(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message { + log.Printf("Got message in handleB: path=%q: %#v from %v", m.Path(), m, a) + if m.IsConfirmable() { //判断是否为CON数据 + res := &coap.Message{ + Type: coap.Acknowledgement, //指定回复数据为ACK类型 + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("good bye!"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) //设置Option ContentFormat + + log.Printf("Transmitting from B %#v", res) + return res + } + return nil +} + + +func main() { + + mux := coap.NewServeMux() + mux.Handle("/a", coap.FuncHandler(handleA)) //创建 "/a"处理接口 + mux.Handle("/b", coap.FuncHandler(handleB)) //创建 "/b"处理接口 + + log.Fatal(coap.ListenAndServe("udp", ":5683", mux)) //启动Server 端口为5683 这里为什么要用":5683"?搞不明白 + +} diff --git a/protocol/http/go.mod b/protocol/http/go.mod new file mode 100644 index 0000000..98b5312 --- /dev/null +++ b/protocol/http/go.mod @@ -0,0 +1,33 @@ +module iot-http + +go 1.22.4 + +require ( + github.com/bytedance/sonic v1.11.9 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/protocol/http/go.sum b/protocol/http/go.sum new file mode 100644 index 0000000..3c995bd --- /dev/null +++ b/protocol/http/go.sum @@ -0,0 +1,79 @@ +github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= +github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/protocol/http/main.go b/protocol/http/main.go new file mode 100644 index 0000000..5c61254 --- /dev/null +++ b/protocol/http/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + err := r.Run() + if err != nil { + return + } +} \ No newline at end of file diff --git a/modbus/go.mod b/protocol/modbus/go.mod similarity index 100% rename from modbus/go.mod rename to protocol/modbus/go.mod diff --git a/modbus/go.sum b/protocol/modbus/go.sum similarity index 100% rename from modbus/go.sum rename to protocol/modbus/go.sum diff --git a/modbus/client.go b/protocol/modbus/sample/client/client.go similarity index 93% rename from modbus/client.go rename to protocol/modbus/sample/client/client.go index 3b873fb..a704dd8 100644 --- a/modbus/client.go +++ b/protocol/modbus/sample/client/client.go @@ -6,16 +6,14 @@ import ( "github.com/goburrow/modbus" ) -func client() { +func main() { handler := modbus.NewTCPClientHandler("localhost:1502") // Connect manually so that multiple requests are handled in one session err := handler.Connect() defer handler.Close() client := modbus.NewClient(handler) - - - _, err = client.WriteMultipleRegisters(0, 5, []byte{0, 3, 0, 4, 0, 5,1,1,1,2}) + _, err = client.WriteMultipleRegisters(0, 5, []byte{0, 3, 0, 4, 0, 5, 1, 1, 1, 2}) if err != nil { fmt.Printf("%v\n", err) } @@ -26,3 +24,4 @@ func client() { } fmt.Printf("results %v\n", results) } + diff --git a/modbus/server.go b/protocol/modbus/sample/server/server.go similarity index 96% rename from modbus/server.go rename to protocol/modbus/sample/server/server.go index 3042504..09584d5 100644 --- a/modbus/server.go +++ b/protocol/modbus/sample/server/server.go @@ -18,6 +18,5 @@ func main() { // Wait forever for { time.Sleep(1 * time.Second) - client() } } \ No newline at end of file diff --git a/protocol/readme.md b/protocol/readme.md new file mode 100644 index 0000000..fa3a777 --- /dev/null +++ b/protocol/readme.md @@ -0,0 +1,6 @@ +# 扩展协议支持 + +1. coap +2. modbus +3. http + -- Gitee From eeccbb0db8f94cb8c1f8dff8640896a5bd821f0c Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Wed, 24 Jul 2024 11:28:19 +0800 Subject: [PATCH 10/90] =?UTF-8?q?feat:=20tcp=20=E5=8D=8F=E8=AE=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 8 +- go-iot-mq/handler_tcp_storage.go | 144 ++++++++++++++++ iot-go-project/biz/tcp_biz.go | 47 +++++ iot-go-project/initialize/init.go | 30 ++-- iot-go-project/models/models.go | 34 ++-- iot-go-project/router/device_info_router.go | 91 +++++++++- iot-go-project/router/tcp_handler_router.go | 181 ++++++++++++++++++++ iot-go-project/servlet/servlet.go | 5 + protocol/readme.md | 51 ++++++ protocol/tcp/app-local.yml | 16 ++ protocol/tcp/go.mod | 45 +++++ protocol/tcp/go.sum | 116 +++++++++++++ protocol/tcp/main.go | 98 +++++++++++ protocol/tcp/rabbit_mq.go | 81 +++++++++ protocol/tcp/redis.go | 26 +++ protocol/tcp/redis_lock.go | 84 +++++++++ protocol/tcp/server.go | 163 ++++++++++++++++++ protocol/tcp/server_test.go | 11 ++ 18 files changed, 1204 insertions(+), 27 deletions(-) create mode 100644 go-iot-mq/handler_tcp_storage.go create mode 100644 iot-go-project/biz/tcp_biz.go create mode 100644 iot-go-project/router/tcp_handler_router.go create mode 100644 protocol/tcp/app-local.yml create mode 100644 protocol/tcp/go.mod create mode 100644 protocol/tcp/go.sum create mode 100644 protocol/tcp/main.go create mode 100644 protocol/tcp/rabbit_mq.go create mode 100644 protocol/tcp/redis.go create mode 100644 protocol/tcp/redis_lock.go create mode 100644 protocol/tcp/server.go create mode 100644 protocol/tcp/server_test.go diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index 85cc530..26d98e0 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -1,3 +1,5 @@ +// 用于处理MQTT转发过来的数据 + package main import ( @@ -74,7 +76,7 @@ func HandlerDataStorageString(d amqp.Delivery) { } zap.S().Infof("推送报警原始数据: %s", jsonData) writeAPI.Flush() - HandlerLastTime(*data) + HandlerMqttLastTime(*data) PushToQueue("waring_handler", jsonData) PushToQueue("waring_delay_handler", jsonData) PushToQueue("transmit_handler", jsonData) @@ -84,8 +86,8 @@ func HandlerDataStorageString(d amqp.Delivery) { } -// HandlerLastTime 和上一次推送事件进行对比,判断是否超过阈值,如果超过则发送额外的消息通知 -func HandlerLastTime(data []DataRowList) { +// HandlerMqttLastTime 和上一次推送事件进行对比,判断是否超过阈值,如果超过则发送额外的消息通知 +func HandlerMqttLastTime(data []DataRowList) { if len(data) == 0 { return } diff --git a/go-iot-mq/handler_tcp_storage.go b/go-iot-mq/handler_tcp_storage.go new file mode 100644 index 0000000..e162f60 --- /dev/null +++ b/go-iot-mq/handler_tcp_storage.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + "strconv" + "time" +) + +// 用于处理tcp转发后的数据 +type TcpMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +// HandlerDataStorage 函数处理从AMQP通道接收到的MQTT消息数据 +// 参数: +// +// messages <-chan amqp.Delivery:接收AMQP消息的通道 +// +// 返回值: +// +// 无 +func HandlerTcpDataStorage(messages <-chan amqp.Delivery) { + + go func() { + + for d := range messages { + HandlerDataTcpStorageString(d) + err := d.Ack(false) + if err != nil { + zap.S().Errorf("消息确认异常:%+v", err) + + } + } + }() + + zap.S().Infof(" [*] Waiting for messages. To exit press CTRL+C") +} + +func HandlerDataTcpStorageString(d amqp.Delivery) { + var msg TcpMessage + err := json.Unmarshal(d.Body, &msg) + if err != nil { + zap.S().Infof("Failed to unmarshal message: %s", err) + return + } + zap.S().Infof("处理 pre_handler 数据 : %+v", msg) + + script := GetScriptRedisForTcp(msg.Uid) + if script != "" { + data := runScript(msg.Message, script) + for i := 0; i < len(*data); i++ { + row := (*data)[i] + StorageDataRowList(row) + } + zap.S().Debugf("DataRowList: %+v", data) + + jsonData, err := json.Marshal(data) + if err != nil { + zap.S().Errorf("推送报警原始数据异常 %s", err) + return + } + zap.S().Infof("推送报警原始数据: %s", jsonData) + writeAPI.Flush() + HandlerTcpLastTime(*data) + PushToQueue("waring_handler", jsonData) + PushToQueue("waring_delay_handler", jsonData) + PushToQueue("transmit_handler", jsonData) + } else { + zap.S().Infof("执行脚本为空") + } + +} + +// HandlerTcpLastTime 和上一次推送事件进行对比,判断是否超过阈值,如果超过则发送额外的消息通知 +func HandlerTcpLastTime(data []DataRowList) { + if len(data) == 0 { + return + } + + var deviceUid = data[0].DeviceUid + key := "last_push_time:" + deviceUid + // 1. 从redis中获取这个设备上次推送的时间 + lastTime, err := globalRedisClient.Get(context.Background(), key).Result() + if err != nil && !errors.Is(err, redis.Nil) { + zap.S().Errorf("获取设备上次推送时间异常:%+v", err) + return + } + now := time.Now().Unix() + + // 如果没有这个时间则设置时间(当前时间) + if errors.Is(err, redis.Nil) { + err := globalRedisClient.Set(context.Background(), key, now, 0).Err() + if err != nil { + zap.S().Errorf("设置设备上次推送时间异常:%+v", err) + return + } + lastTime = fmt.Sprintf("%d", now) + } + + if lastTime != fmt.Sprintf("%d", now) { + + val := globalRedisClient.LRange(context.Background(), "tcp_bind_device_info:"+deviceUid, 0, -1).Val() + + for _, s := range val { + handlerTcpOne(s) + } + + } + +} + +func handlerTcpOne(deviceUid string) bool { + val := globalRedisClient.Get(context.Background(), "tcp_bind_device_info:"+deviceUid).Val() + if val == "" { + return true + } + parseUint, _ := strconv.ParseUint(val, 10, 64) + withRedis := FindByIdWithRedis(parseUint) + if withRedis == nil { + return true + } + globalRedisClient.Expire(context.Background(), "Device_Off_Message:"+deviceUid, time.Duration(withRedis.PushInterval)*time.Second) + return false +} + +// GetScriptRedisForTcp 根据tcp 的设备ID从Redis中获取对应的脚本 +// 参数: +// +// tcp id string - tcp id +// +// 返回值: +// +// string - 对应的脚本 +func GetScriptRedisForTcp(tcpId string) string { + val := globalRedisClient.HGet(context.Background(), "struct:tcp", tcpId).Val() + return val +} diff --git a/iot-go-project/biz/tcp_biz.go b/iot-go-project/biz/tcp_biz.go new file mode 100644 index 0000000..2e33553 --- /dev/null +++ b/iot-go-project/biz/tcp_biz.go @@ -0,0 +1,47 @@ +package biz + +import ( + "context" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type TcpHandlerBiz struct{} + +func (biz *TcpHandlerBiz) ById(id uint) (*models.TcpHandler, error) { + var TcpHandler models.TcpHandler + + result := glob.GDb.First(&TcpHandler, id) + if result.Error != nil { + return nil, result.Error + } + return &TcpHandler, nil +} + +func (biz *TcpHandlerBiz) PageData(name string, page, size int) (*servlet.PaginationQ, error) { + var pagination servlet.PaginationQ + var TcpHandlerList []models.TcpHandler + + db := glob.GDb + if name != "" { + db = db.Where("name LIKE ?", "%"+name+"%") + } + + db.Model(&models.TcpHandler{}).Count(&pagination.Total) // 计算总记录数 + offset := (page - 1) * size + db.Offset(offset).Limit(size).Find(&TcpHandlerList) + pagination.Data = TcpHandlerList + pagination.Page = page + pagination.Size = size + + return &pagination, nil +} + +func (biz *TcpHandlerBiz) SetRedis(data models.TcpHandler) { + glob.GRedis.HSet(context.Background(), "struct:tcp", strconv.Itoa(int(data.ID)), data.Script) +} +func (biz *TcpHandlerBiz) RemoveRedis(data models.TcpHandler) { + glob.GRedis.HDel(context.Background(), "struct:tcp", strconv.Itoa(int(data.ID))) +} diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index 59031f6..a7c5b8c 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -60,9 +60,10 @@ var ( clickTransmitApi = transmit.ClickhouseTransmitApi{} cassandraTransmitApi = transmit.CassandraTransmitApi{} - - feishuApi = notice.FeiShuApi{} + feishuApi = notice.FeiShuApi{} dingdingApi = notice.DingDingApi{} + + tcpHandlerApi = router.TcpHandlerApi{} ) func initTable() { @@ -335,6 +336,13 @@ func initTable() { zap.S().Errorf("数据库表创建失败 %+v", err) } } + if !glob.GDb.Migrator().HasTable(&models.TcpHandler{}) { + + err := glob.GDb.AutoMigrate(&models.TcpHandler{}) + if err != nil { + zap.S().Errorf("数据库表创建失败 %+v", err) + } + } } func initDb() { @@ -359,9 +367,7 @@ func initDb() { } func initMongo() { - connStr := fmt.Sprintf("mongodb://%s:%s@%s:%d", url.QueryEscape(glob.GConfig.MongoConfig.Username), - url.QueryEscape(glob.GConfig.MongoConfig.Password), glob.GConfig.MongoConfig.Host, - glob.GConfig.MongoConfig.Port) + connStr := fmt.Sprintf("mongodb://%s:%s@%s:%d", url.QueryEscape(glob.GConfig.MongoConfig.Username), url.QueryEscape(glob.GConfig.MongoConfig.Password), glob.GConfig.MongoConfig.Host, glob.GConfig.MongoConfig.Port) client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(connStr)) if err != nil { log.Fatal(err) @@ -511,6 +517,7 @@ func initRouter(r *gin.RouterGroup) { r.GET("/DeviceInfo/page", deviceInfoApi.PageDeviceInfo) r.POST("/DeviceInfo/delete/:id", deviceInfoApi.DeleteDeviceInfo) r.POST("/DeviceInfo/BindMqtt", deviceInfoApi.BindMqtt) + r.POST("/DeviceInfo/BindTcp", deviceInfoApi.BindTcp) r.POST("/DeviceInfo/QueryBindMqtt", deviceInfoApi.QueryBindMqtt) r.POST("/ProductionPlan/create", productionPlanApi.CreateProductionPlan) @@ -598,11 +605,6 @@ func initRouter(r *gin.RouterGroup) { r.GET("/MessageList/page", messageListApi.PageMessageList) - - - - - r.POST("/DingDing/create", dingdingApi.CreateDingDing) r.POST("/DingDing/update", dingdingApi.UpdateDingDing) r.GET("/DingDing/:id", dingdingApi.ByIdDingDing) @@ -610,8 +612,6 @@ func initRouter(r *gin.RouterGroup) { r.POST("/DingDing/delete/:id", dingdingApi.DeleteDingDing) r.POST("/DingDing/bind", dingdingApi.Bind) - - r.POST("/FeiShuId/create", feishuApi.CreateFeiShu) r.POST("/FeiShuId/update", feishuApi.UpdateFeiShu) r.GET("/FeiShuId/:id", feishuApi.ByIdFeiShu) @@ -619,6 +619,12 @@ func initRouter(r *gin.RouterGroup) { r.POST("/FeiShuId/delete/:id", feishuApi.DeleteFeiShu) r.POST("/FeiShuId/bind", feishuApi.Bind) + r.POST("/TcpHandler/create", tcpHandlerApi.CreateTcpHandler) + r.POST("/TcpHandler/update", tcpHandlerApi.UpdateTcpHandler) + r.GET("/TcpHandler/:id", tcpHandlerApi.ByIdTcpHandler) + r.GET("/TcpHandler/page", tcpHandlerApi.PageTcpHandler) + r.POST("/TcpHandler/delete/:id", tcpHandlerApi.DeleteTcpHandler) + } func initGlobalRedisClient() { diff --git a/iot-go-project/models/models.go b/iot-go-project/models/models.go index b55a1d1..064938e 100644 --- a/iot-go-project/models/models.go +++ b/iot-go-project/models/models.go @@ -6,16 +6,16 @@ import ( ) type MqttClient struct { - Host string `json:"host"` // 主机 - Port int `json:"port"` // 端口 - ClientId string `json:"client_id"` // 客户端id - Username string `json:"username"` // 账号 - Password string `json:"password"` // 密码 - Subtopic string `json:"subtopic"` // 订阅的主题 - Start bool `json:"start"` // 是否启动 -LastPushTime string `json:"last_push_time" gorm:"-"` // 最后推送时间 - Script string `json:"script" gorm:"type:text"` // 数据处理脚本 - gorm.Model `structs:"-"` + Host string `json:"host"` // 主机 + Port int `json:"port"` // 端口 + ClientId string `json:"client_id"` // 客户端id + Username string `json:"username"` // 账号 + Password string `json:"password"` // 密码 + Subtopic string `json:"subtopic"` // 订阅的主题 + Start bool `json:"start"` // 是否启动 + LastPushTime string `json:"last_push_time" gorm:"-"` // 最后推送时间 + Script string `json:"script" gorm:"type:text"` // 数据处理脚本 + gorm.Model `structs:"-"` } type Signal struct { @@ -118,7 +118,7 @@ type DeviceInfo struct { ProcurementDate *time.Time `json:"procurement_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"procurement_date"` // 采购日期 Source int `json:"source" structs:"source"` // 设备来源,1: 内部,2: 外源 WarrantyExpiry *time.Time `json:"warranty_expiry,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"warranty_expiry"` // 保修截止日期 - PushInterval int `json:"push_interval" structs:"push_interval"` // 推送间隔(秒) + PushInterval int `json:"push_interval" structs:"push_interval"` // 推送间隔(秒) ErrorRate float64 `json:"error_rate" structs:"error_rate"` // 推送时间误差(秒) gorm.Model `structs:"-"` } @@ -242,7 +242,19 @@ type DeviceBindMqttClient struct { gorm.Model `structs:"-"` DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID MqttClientId uint `json:"mqtt_client_id" structs:"mqtt_client_id"` // MQTT客户端表的外键ID +} +type DeviceBindTcpHandler struct { + gorm.Model `structs:"-"` + DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID + TcpHandlerId uint `json:"tcp_handler_id" structs:"tcp_handler_id"` // TCP处理器的ID +} + +// TcpHandler 表示TCP数据处理器 +type TcpHandler struct { + gorm.Model `structs:"-"` + Name string `json:"name" structs:"name"` // 处理器名 + Script string `json:"script" structs:"script"` // 处理器脚本 } type DeviceGroupBindMqttClient struct { diff --git a/iot-go-project/router/device_info_router.go b/iot-go-project/router/device_info_router.go index 5f08b0a..fa617da 100644 --- a/iot-go-project/router/device_info_router.go +++ b/iot-go-project/router/device_info_router.go @@ -233,7 +233,7 @@ func (api *DeviceInfoApi) QueryBindMqtt(c *gin.Context) { // @Summary 绑定mqtt客户端 // @Accept json // @Produce json -// @Param DeviceGroup body servlet.DeviceGroupCreateParam true "绑定参数" +// @Param DeviceGroup body servlet.DeviceBindMqttClientParam true "绑定参数" // @Router /DeviceInfo/BindMqtt [post] func (api *DeviceInfoApi) BindMqtt(c *gin.Context) { var param servlet.DeviceBindMqttClientParam @@ -314,3 +314,92 @@ func (api *DeviceInfoApi) BindMqtt(c *gin.Context) { servlet.Resp(c, "绑定成功") } + + + +// BindTcp +// @Tags DeviceInfos +// @Summary 绑定tcp处理器 +// @Accept json +// @Produce json +// @Param DeviceGroup body servlet.DeviceBindTcpParam true "绑定参数" +// @Router /DeviceInfo/BindTcp [post] +func (api *DeviceInfoApi) BindTcp(c *gin.Context) { + var param servlet.DeviceBindTcpParam + if err := c.ShouldBindJSON(¶m); err != nil { + + servlet.Error(c, err.Error()) + return + } + + // 开启事务 + tx := glob.GDb.Begin() + if tx.Error != nil { + servlet.Error(c, "Failed to begin transaction") + return + } + var toDel []models.DeviceBindTcpHandler + + tx.Where("`device_info_id` = ?", param.DeviceId).Find(toDel) + + result := tx.Where("`device_info_id` = ?", param.DeviceId).Delete(&models.DeviceBindMqttClient{}) + + if result.Error != nil { + // 如果出现错误,回滚事务 + tx.Rollback() + servlet.Error(c, "Error occurred during deletion") + return + } + + var deviceBindTcpHandlers []models.DeviceBindTcpHandler + for _, tcpId := range param.TcpHandlerId { + deviceBindTcpHandlers = append(deviceBindTcpHandlers, models.DeviceBindTcpHandler{ + DeviceInfoId: uint(param.DeviceId), + TcpHandlerId: uint(tcpId), + }) + } + + result = tx.Model(&models.DeviceBindTcpHandler{}).CreateInBatches(deviceBindTcpHandlers, len(deviceBindTcpHandlers)) + if result.Error != nil { + tx.Rollback() + zap.S().Infoln("Error occurred during creation:", result.Error) + servlet.Error(c, "Error occurred during creation") + return + } + if err := tx.Commit().Error; err != nil { + servlet.Error(c, "Failed to commit transaction") + return + } + + // redis 中建立 tcp 与 device_info_id 的映射 + + var DeviceInfo models.DeviceInfo + + first := tx.First(&DeviceInfo, param.DeviceId) + if first.Error != nil { + servlet.Error(c, "DeviceInfo not found") + return + } + + for _, client := range toDel { + glob.GRedis.Del(context.Background(), "tcp_bind_product:"+strconv.Itoa(int(client.TcpHandlerId))) + } + + for _, item := range param.TcpHandlerId { + glob.GRedis.LPush(context.Background(), "tcp_bind_product:"+strconv.Itoa(item), DeviceInfo.ProductId) + } + + + for _, client := range toDel { + glob.GRedis.Del(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(int(client.TcpHandlerId))) + } + + for _, item := range param.TcpHandlerId { + glob.GRedis.LPush(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(item), DeviceInfo.ID) + + } + + + servlet.Resp(c, "绑定成功") + +} \ No newline at end of file diff --git a/iot-go-project/router/tcp_handler_router.go b/iot-go-project/router/tcp_handler_router.go new file mode 100644 index 0000000..7f6c8bf --- /dev/null +++ b/iot-go-project/router/tcp_handler_router.go @@ -0,0 +1,181 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "igp/biz" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type TcpHandlerApi struct{} + +var TcpHandlerBiz = biz.TcpHandlerBiz{} + +// CreateTcpHandler +// @Summary 创建Tcp数据处理器 +// @Description 创建Tcp数据处理器 +// @Tags TcpHandlers +// @Accept json +// @Produce json +// @Param TcpHandler body models.TcpHandler true "Tcp数据处理器" +// @Success 201 {object} servlet.JSONResult{data=models.TcpHandler} "创建成功的Tcp数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 500 {string} string "内部服务器错误" +// @Router /TcpHandler/create [post] +func (api *TcpHandlerApi) CreateTcpHandler(c *gin.Context) { + var TcpHandler models.TcpHandler + if err := c.ShouldBindJSON(&TcpHandler); err != nil { + servlet.Error(c, err.Error()) + return + } + + // 检查 TcpHandler 是否被正确初始化 + if TcpHandler.Name == "" { + servlet.Error(c, "名称不能为空") + return + } + + result := glob.GDb.Create(&TcpHandler) + + if result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + TcpHandlerBiz.SetRedis(TcpHandler) + // 返回创建成功的Tcp数据处理器 + servlet.Resp(c, TcpHandler) +} + +// UpdateTcpHandler +// @Summary 更新一个Tcp数据处理器 +// @Description 更新一个Tcp数据处理器 +// @Tags TcpHandlers +// @Accept json +// @Produce json +// @Param TcpHandler body models.TcpHandler true "Tcp数据处理器" +// @Success 200 {object} servlet.JSONResult{data=models.TcpHandler} "Tcp数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 404 {string} string "Tcp数据处理器未找到" +// @Failure 500 {string} string "内部服务器错误" +// @Router /TcpHandler/update [post] +func (api *TcpHandlerApi) UpdateTcpHandler(c *gin.Context) { + var req models.TcpHandler + if err := c.ShouldBindJSON(&req); err != nil { + + servlet.Error(c, err.Error()) + return + } + + var old models.TcpHandler + result := glob.GDb.First(&old, req.ID) + if result.Error != nil { + + servlet.Error(c, "TcpHandler not found") + return + } + + var newV models.TcpHandler + newV = old + newV.Name = req.Name + newV.Script = req.Script + result = glob.GDb.Model(&newV).Updates(newV) + + if result.Error != nil { + + servlet.Error(c, result.Error.Error()) + return + } + TcpHandlerBiz.SetRedis(newV) + servlet.Resp(c, old) +} + +// PageTcpHandler +// @Summary 分页查询Tcp数据处理器 +// @Description 分页查询Tcp数据处理器 +// @Tags TcpHandlers +// @Accept json +// @Produce json +// @Param name query string false "Tcp数据处理器名称" +// @Param pid query int false "上级id" +// @Param page query int false "页码" default(0) +// @Param page_size query int false "每页大小" default(10) +// @Success 200 {object} servlet.JSONResult{data=servlet.PaginationQ{data=models.TcpHandler}} "Tcp数据处理器" +// @Failure 400 {string} string "请求参数错误" +// @Failure 500 {string} string "查询异常" +// @Router /TcpHandler/page [get] +func (api *TcpHandlerApi) PageTcpHandler(c *gin.Context) { + var name = c.Query("name") + var page = c.DefaultQuery("page", "0") + var pageSize = c.DefaultQuery("page_size", "10") + parseUint, err := strconv.Atoi(page) + if err != nil { + servlet.Error(c, "无效的页码") + return + } + u, err := strconv.Atoi(pageSize) + + if err != nil { + servlet.Error(c, "无效的页长") + return + } + + data, err := TcpHandlerBiz.PageData(name, parseUint, u) + if err != nil { + servlet.Error(c, "查询异常") + return + } + servlet.Resp(c, data) +} + +// DeleteTcpHandler +// @Tags TcpHandlers +// @Summary 删除Tcp数据处理器 +// @Produce application/json +// @Param id path int true "主键" +// @Router /TcpHandler/delete/:id [post] +func (api *TcpHandlerApi) DeleteTcpHandler(c *gin.Context) { + var TcpHandler models.TcpHandler + + param := c.Param("id") + + result := glob.GDb.First(&TcpHandler, param) + if result.Error != nil { + servlet.Error(c, "TcpHandler not found") + + return + } + + if result := glob.GDb.Delete(&TcpHandler); result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + TcpHandlerBiz.RemoveRedis(TcpHandler) + + servlet.Resp(c, "删除成功") +} + +// ByIdTcpHandler +// @Tags TcpHandlers +// @Summary 单个详情 +// @Param id path int true "主键" +// @Produce application/json +// @Router /TcpHandler/:id [get] +func (api *TcpHandlerApi) ByIdTcpHandler(c *gin.Context) { + var TcpHandler models.TcpHandler + + param := c.Param("id") + + result := glob.GDb.First(&TcpHandler, param) + if result.Error != nil { + servlet.Error(c, "TcpHandler not found") + + return + } + + servlet.Resp(c, TcpHandler) +} + + + diff --git a/iot-go-project/servlet/servlet.go b/iot-go-project/servlet/servlet.go index 2f02362..c0cb72a 100644 --- a/iot-go-project/servlet/servlet.go +++ b/iot-go-project/servlet/servlet.go @@ -287,6 +287,11 @@ type DeviceBindMqttClientParam struct { DeviceId int `json:"device_id"` MqttClientId []int `json:"mqtt_client_id"` } +type DeviceBindTcpParam struct { + DeviceId int `json:"device_id"` + TcpHandlerId []int `json:"tcp_handler_id"` +} + type DeviceGroupBindMqttClientParam struct { DeviceGroupId uint `json:"device_group_id" structs:"device_group_id"` // 设备组ID diff --git a/protocol/readme.md b/protocol/readme.md index fa3a777..512d23a 100644 --- a/protocol/readme.md +++ b/protocol/readme.md @@ -4,3 +4,54 @@ 2. modbus 3. http +coap 无法直接支持用户认证 , 需要通过一个额外的接口进行判断 + + +http 可以支持用户认证 , 请求头中直接携带标识即可 +1. username + password + + +## TCP + +- nginx 配置 + +``` +stream{ + + upstream tcpserver { + server 0.0.0.0:3333; # 实例端口 + server 0.0.0.0:3332; + } + server { + listen 22122; + proxy_pass tcpserver; + } +} +``` + +识别码建立过程 + +1. 执行如下指令完成TCP链接建立 + +``` +nc -v 127.0.0.1 22122 +``` + +2. 发送`uid:`开头的数据,用于确认具体的设备唯一编码 +3. 发送实际数据进行处理 + + + +- 完整TCP请求案例 + +``` +nc -v 127.0.0.1 22122 +Connection to 127.0.0.1 port 22122 [tcp/*] succeeded! +1 +请发送uid:xxx格式的消息进行设备ID映射。 +uid:1 +成功识别设备编码. +datadata +数据已处理. +``` + diff --git a/protocol/tcp/app-local.yml b/protocol/tcp/app-local.yml new file mode 100644 index 0000000..be293e2 --- /dev/null +++ b/protocol/tcp/app-local.yml @@ -0,0 +1,16 @@ +node_info: + host: 127.0.0.1 + port: 3332 +redis_config: + host: 127.0.0.1 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest \ No newline at end of file diff --git a/protocol/tcp/go.mod b/protocol/tcp/go.mod new file mode 100644 index 0000000..1581387 --- /dev/null +++ b/protocol/tcp/go.mod @@ -0,0 +1,45 @@ +module iot-tcp + +go 1.22.4 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/redis/go-redis/v9 v9.6.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/protocol/tcp/go.sum b/protocol/tcp/go.sum new file mode 100644 index 0000000..fa1bef5 --- /dev/null +++ b/protocol/tcp/go.sum @@ -0,0 +1,116 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= +github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/protocol/tcp/main.go b/protocol/tcp/main.go new file mode 100644 index 0000000..50a347a --- /dev/null +++ b/protocol/tcp/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + "os" + "strconv" +) +var globalConfig ServerConfig + +func main() { + var configPath string + flag.StringVar(&configPath, "config", "app-local.yml", "Path to the config file") + flag.Parse() + + yfile, err := os.ReadFile(configPath) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + err = yaml.Unmarshal(yfile, &globalConfig) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + zap.S().Infof("node name = %v , host = %v , port = %v", globalConfig.NodeInfo.Name, globalConfig.NodeInfo.Host, globalConfig.NodeInfo.Port) + InitRabbitCon() + + server := New(&Config{ + Host: "localhost", + Port: strconv.Itoa(globalConfig.NodeInfo.Port), + }) + server.Run() +} + + + + +// ServerConfig 定义了服务器配置的结构体,包含了节点信息、Redis配置和消息队列配置。 +type ServerConfig struct { + // NodeInfo 定义了节点的信息,包括主机地址、端口、节点名称、节点类型和最大处理数量。 + NodeInfo NodeInfo `yaml:"node_info" json:"node_info"` + + // RedisConfig 定义了Redis服务器的配置,包括主机地址、端口、数据库索引和密码。 + RedisConfig RedisConfig `yaml:"redis_config" json:"redis_config"` + + // MQConfig 定义了消息队列服务器的配置,包括主机地址、端口、用户名和密码。 + MQConfig MQConfig `yaml:"mq_config" json:"mq_config"` +} + +// NodeInfo 定义了节点的基本信息。 +type NodeInfo struct { + // Host 表示节点的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示节点监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Name 表示节点的名称。 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Type 表示节点的类型。 + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // Size 表示节点可以处理的最大数量。 + Size int64 `json:"size,omitempty" yaml:"size,omitempty"` +} + +// RedisConfig 定义了Redis服务器的配置信息。 +type RedisConfig struct { + // Host 表示Redis服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示Redis服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Db 表示Redis服务器的数据库索引。 + Db int `json:"db,omitempty" yaml:"db,omitempty"` + + // Password 表示Redis服务器的访问密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} + +// MQConfig 定义了消息队列服务器的配置信息。 +type MQConfig struct { + // Host 表示消息队列服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示消息队列服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Username 表示用于访问消息队列服务器的用户名。 + Username string `json:"username,omitempty" yaml:"username,omitempty"` + + // Password 表示用于访问消息队列服务器的密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} \ No newline at end of file diff --git a/protocol/tcp/rabbit_mq.go b/protocol/tcp/rabbit_mq.go new file mode 100644 index 0000000..f97222d --- /dev/null +++ b/protocol/tcp/rabbit_mq.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "go.uber.org/zap" +) + +var GRabbitMq *amqp.Connection + +func CreateRabbitQueue(queueName string) { + + ch, err := GRabbitMq.Channel() + if err != nil { + zap.S().Fatalf("Failed to open a channel %v", err) + } + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _, err = ch.QueueDeclare(queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + zap.S().Fatalf("创建queue异常 %s", queueName) + } +} +func InitRabbitCon() { + conn, err := amqp.Dial(genUrl()) + if err != nil { + zap.S().Fatalf("Failed to connect to RabbitMQ %v", err) + } + + GRabbitMq = conn + + CreateRabbitQueue("pre_tcp_handler") + +} +func genUrl() string { + connStr := fmt.Sprintf("amqp://%s:%s@%s:%d/", globalConfig.MQConfig.Username, globalConfig.MQConfig.Password, globalConfig.MQConfig.Host, globalConfig.MQConfig.Port) + return connStr +} + +// PushToQueue 将消息推送到RabbitMQ队列中 +// +// 参数: +// queue_name: string类型,目标队列的名称 +// body: []byte类型,待发送的消息体 +// +// 返回值: +// 无返回值 +func PushToQueue(queueName string, body []byte) { + + ch, _ := GRabbitMq.Channel() + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _ = ch.PublishWithContext(context.Background(), "", queueName, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: body, + }) + zap.S().Infof(" [x] 发送到 %s 消息体 %s", queueName, body) + +} diff --git a/protocol/tcp/redis.go b/protocol/tcp/redis.go new file mode 100644 index 0000000..2eeb794 --- /dev/null +++ b/protocol/tcp/redis.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +var globalRedisClient *redis.Client + +func initGlobalRedisClient(config RedisConfig) { + + add := fmt.Sprintf("%s:%d", config.Host, config.Port) + globalRedisClient = redis.NewClient(&redis.Options{ + Addr: add, + Password: config.Password, // 如果没有设置密码,就留空字符串 + DB: config.Db, // 使用默认数据库 + }) + + // 检查连接是否成功 + if err := globalRedisClient.Ping(context.Background()).Err(); err != nil { + zap.S().Fatalf("Could not connect to Redis: %v", err) + } + +} diff --git a/protocol/tcp/redis_lock.go b/protocol/tcp/redis_lock.go new file mode 100644 index 0000000..bf10c5b --- /dev/null +++ b/protocol/tcp/redis_lock.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "github.com/redis/go-redis/v9" + "sync" + "time" + + "github.com/google/uuid" +) + +const ( + LockTime = 4 * time.Second + RsDistlockNs = "tdln:" + ReleaseLockLua = ` + if redis.call('get',KEYS[1])==ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + ` +) + +type RedisDistLock struct { + id string + lockName string + redisClient *redis.Client + m sync.Mutex +} + +func NewRedisDistLock(redisClient *redis.Client, lockName string) *RedisDistLock { + return &RedisDistLock{ + lockName: lockName, + redisClient: redisClient, + } +} + +func (lock *RedisDistLock) Lock() { + for !lock.TryLock() { + time.Sleep(5 * time.Second) + } +} + +func (lock *RedisDistLock) TryLock() bool { + if lock.id != "" { + // 处于加锁中 + return false + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id != "" { + // 处于加锁中 + return false + } + ctx := context.Background() + id := uuid.New().String() + reply := lock.redisClient.SetNX(ctx, RsDistlockNs+lock.lockName, id, LockTime) + if reply.Err() == nil && reply.Val() { + lock.id = id + return true + } + + return false +} + +func (lock *RedisDistLock) Unlock() { + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + ctx := context.Background() + reply := lock.redisClient.Eval(ctx, ReleaseLockLua, []string{RsDistlockNs + lock.lockName}, lock.id) + if reply.Err() != nil { + panic("释放锁失败!") + } else { + lock.id = "" + } +} diff --git a/protocol/tcp/server.go b/protocol/tcp/server.go new file mode 100644 index 0000000..95500a9 --- /dev/null +++ b/protocol/tcp/server.go @@ -0,0 +1,163 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "go.uber.org/zap" + "log" + "net" + "strings" + "sync" +) + +type Server struct { + host string + port string + deviceIdMap map[string]*Client + remoteIpMap map[string]string + mu sync.Mutex // 保护deviceIdMap的互斥锁 +} + +type Client struct { + conn net.Conn +} + +type Config struct { + Host string + Port string +} + +func New(config *Config) *Server { + return &Server{ + host: config.Host, + port: config.Port, + deviceIdMap: make(map[string]*Client), + remoteIpMap: make(map[string]string), + } +} + +func (server *Server) Run() { + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", server.host, server.port)) + if err != nil { + log.Fatal(err) + } + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + log.Fatal(err) + } + + client := &Client{ + conn: conn, + } + go server.handleClient(client) + } +} + +func (server *Server) handleClient(client *Client) { + defer func(conn net.Conn) { + log.Printf("Client %s disconnected.\n", conn.RemoteAddr()) + + server.mu.Lock() + defer server.mu.Unlock() + + // 更新或添加设备ID映射 + s := server.remoteIpMap[client.conn.RemoteAddr().String()] + delete(server.deviceIdMap, s) + delete(server.remoteIpMap, client.conn.RemoteAddr().String()) + err := conn.Close() + if err != nil { + + } + }(client.conn) + + reader := bufio.NewReader(client.conn) + for { + message, err := reader.ReadString('\n') + if err != nil { + return // 关闭连接并退出goroutine + } + + server.handleMessage(client, message) + } +} + +func (server *Server) handleMessage(client *Client, message string) { + + // 判断这个客户端是否建立过uid映射,没有的话不处理数据 + + _, ok := server.remoteIpMap[client.conn.RemoteAddr().String()] + + if ok { + handlerData(server,message, client) + } else { + + deviceId := handlerUid(message) + if deviceId == "" { + clientWrite(client, "请发送uid:xxx格式的消息进行设备ID映射。\n") + + return + } else { + server.mu.Lock() + defer server.mu.Unlock() + + // 更新或添加设备ID映射 + server.deviceIdMap[deviceId] = client + server.remoteIpMap[client.conn.RemoteAddr().String()] = deviceId + clientWrite(client, "成功识别设备编码.\n") + + return + + } + + } + +} + +type TcpMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +func handlerData(server *Server, message string, client *Client) { + println(message) + + zap.S().Debugf("处理消息: %s 客户端: %s\n", message, client.conn.RemoteAddr().String()) + + s := server.remoteIpMap[client.conn.RemoteAddr().String()] + + // 创建 MQTTMessage 实例并序列化为 JSON + mqttMsg := TcpMessage{ + Uid: s, + Message: message, + } + jsonData, err := json.Marshal(mqttMsg) + if err != nil { + zap.S().Errorf("Error marshalling MQTT message to JSON: %v", err) + return + } + PushToQueue("pre_tcp_handler", jsonData) + + + clientWrite(client, "数据已处理.\n") + +} + +func clientWrite(client *Client, msg string) { + _, err := client.conn.Write([]byte(msg)) + if err != nil { + zap.S().Error(err) + return + } +} + +// handlerUid 用于从消息中提取设备ID +func handlerUid(message string) string { + if strings.HasPrefix(message, "uid:") { + return strings.TrimSpace(message[4:]) + } + return "" +} diff --git a/protocol/tcp/server_test.go b/protocol/tcp/server_test.go new file mode 100644 index 0000000..81b98af --- /dev/null +++ b/protocol/tcp/server_test.go @@ -0,0 +1,11 @@ +package main + +import "testing" + +func TestServer(t *testing.T) { + server := New(&Config{ + Host: "localhost", + Port: "3333", + }) + server.Run() +} \ No newline at end of file -- Gitee From f542da5abf948e3461911811d41118838182f84d Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 12:56:53 +0800 Subject: [PATCH 11/90] =?UTF-8?q?fix:base-env-docker-compose=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dinflux=E4=B8=ADenv=5Ffile=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/env/base-env-docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/env/base-env-docker-compose.yml b/docker/env/base-env-docker-compose.yml index dfcbef8..01fd062 100644 --- a/docker/env/base-env-docker-compose.yml +++ b/docker/env/base-env-docker-compose.yml @@ -20,7 +20,7 @@ services: # Mount for telegraf config - ./influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro env_file: - - influx/influxv2.env + - ./influx/influxv2.env networks: - iot-net mongodb: -- Gitee From 82216a59af1910eb613392ae5cdfcd37ba85c753 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 13:14:18 +0800 Subject: [PATCH 12/90] =?UTF-8?q?fix:IotGoProject.DockerFile=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E6=A1=A3=E7=94=9F=E6=88=90=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/IotGoProject.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/IotGoProject.Dockerfile b/deploy/IotGoProject.Dockerfile index 6eea5a2..1968119 100644 --- a/deploy/IotGoProject.Dockerfile +++ b/deploy/IotGoProject.Dockerfile @@ -11,7 +11,7 @@ COPY ../notice ./notice COPY ../transmit ./transmit -RUN cd iot-go-project && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . +RUN cd iot-go-project && go mod tidy && swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . RUN chmod +x /app/iot-go-project/main # 运行阶段指定 scratch 作为基础镜像 -- Gitee From e1607efab000983fc2466bc04de4418b85cc65ef Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 13:37:08 +0800 Subject: [PATCH 13/90] =?UTF-8?q?fix:=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/.env.docker | 4 ++-- docker/app/iot-project/config/app-local.yml | 10 +++++----- docker/app/mq/config/app-local-calc.yml | 10 +++++----- docker/app/mq/config/app-local-pre_handler.yml | 10 +++++----- docker/app/mq/config/app-local-waring_handler.yml | 10 +++++----- docker/app/mq/config/app-local-wd.yml | 10 +++++----- docker/app/mqtt/config/app-local.yml | 6 +++--- docker/app/mqtt/config/app-local2.yml | 6 +++--- docker/app/mqtt/config/app-local3.yml | 6 +++--- docker/env/base-env-docker-compose.yml | 2 +- go-iot-mq/main.go | 1 + 11 files changed, 38 insertions(+), 37 deletions(-) diff --git a/ant-vue/.env.docker b/ant-vue/.env.docker index aa811f8..122f11c 100644 --- a/ant-vue/.env.docker +++ b/ant-vue/.env.docker @@ -1,6 +1,6 @@ NODE_ENV=docker # 静态文件路径 VITE_BASE_URL= -VITE_APP_ENV_NAME=线上环境 -VITE_APP_API_URL=http://192.168.3.110:8005 +VITE_APP_ENV_NAME=docker环境 +VITE_APP_API_URL=http://172.17.0.1:8005 VITE_LOGIN=some diff --git a/docker/app/iot-project/config/app-local.yml b/docker/app/iot-project/config/app-local.yml index 247e3d9..11a2d93 100644 --- a/docker/app/iot-project/config/app-local.yml +++ b/docker/app/iot-project/config/app-local.yml @@ -1,18 +1,18 @@ node_info: port: 8080 redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest influx_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 8086 token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== org: myorg @@ -21,11 +21,11 @@ influx_config: mysql_config: username: app password: iot123456 - host: 192.168.7.41 + host: 172.17.0.1 port: 3306 dbname: iot mongo_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 27017 username: admin password: admin diff --git a/docker/app/mq/config/app-local-calc.yml b/docker/app/mq/config/app-local-calc.yml index b9391db..09d4447 100644 --- a/docker/app/mq/config/app-local-calc.yml +++ b/docker/app/mq/config/app-local-calc.yml @@ -1,12 +1,12 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 29001 name: mq1 type: calc_queue # pre_handler、 waring_handler、 calc_queue、waring_delay_handler redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 @@ -14,18 +14,18 @@ redis_config: mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest influx_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 8086 token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== org: myorg bucket: buc mongo_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 27017 username: admin password: admin diff --git a/docker/app/mq/config/app-local-pre_handler.yml b/docker/app/mq/config/app-local-pre_handler.yml index 009252a..bdb8df1 100644 --- a/docker/app/mq/config/app-local-pre_handler.yml +++ b/docker/app/mq/config/app-local-pre_handler.yml @@ -1,30 +1,30 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 29002 name: mq1 type: pre_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest influx_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 8086 token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== org: myorg bucket: buc mongo_config: - host: 192.168.7.111 + host: 172.17.0.1 port: 27017 username: admin password: admin diff --git a/docker/app/mq/config/app-local-waring_handler.yml b/docker/app/mq/config/app-local-waring_handler.yml index 57ff2e6..0355d73 100644 --- a/docker/app/mq/config/app-local-waring_handler.yml +++ b/docker/app/mq/config/app-local-waring_handler.yml @@ -1,30 +1,30 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 29003 name: mq1 type: waring_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest influx_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 8086 token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== org: myorg bucket: buc mongo_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 27017 username: admin password: admin diff --git a/docker/app/mq/config/app-local-wd.yml b/docker/app/mq/config/app-local-wd.yml index afefdeb..bb2335b 100644 --- a/docker/app/mq/config/app-local-wd.yml +++ b/docker/app/mq/config/app-local-wd.yml @@ -1,30 +1,30 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 29004 name: mq1 type: waring_delay_handler # pre_handler、 waring_handler、 calc_queue、waring_delay_handler redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest influx_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 8086 token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== org: myorg bucket: buc mongo_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 27017 username: admin password: admin diff --git a/docker/app/mqtt/config/app-local.yml b/docker/app/mqtt/config/app-local.yml index 8a12bd9..1a57e27 100644 --- a/docker/app/mqtt/config/app-local.yml +++ b/docker/app/mqtt/config/app-local.yml @@ -1,11 +1,11 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 8081 name: m1 type: mqtt size: 3 redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 @@ -13,7 +13,7 @@ redis_config: mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest diff --git a/docker/app/mqtt/config/app-local2.yml b/docker/app/mqtt/config/app-local2.yml index c0bf36f..9bf6f25 100644 --- a/docker/app/mqtt/config/app-local2.yml +++ b/docker/app/mqtt/config/app-local2.yml @@ -1,11 +1,11 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 8082 name: m2 type: mqtt size: 3 redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 @@ -13,7 +13,7 @@ redis_config: mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest diff --git a/docker/app/mqtt/config/app-local3.yml b/docker/app/mqtt/config/app-local3.yml index 0b8e08f..eeebc40 100644 --- a/docker/app/mqtt/config/app-local3.yml +++ b/docker/app/mqtt/config/app-local3.yml @@ -1,11 +1,11 @@ node_info: - host: 192.168.7.41 + host: 172.17.0.1 port: 8081 name: m3 type: mqtt size: 3 redis_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 6379 db: 10 password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 @@ -13,7 +13,7 @@ redis_config: mq_config: - host: 192.168.7.41 + host: 172.17.0.1 port: 5672 username: guest password: guest diff --git a/docker/env/base-env-docker-compose.yml b/docker/env/base-env-docker-compose.yml index 01fd062..7a49f8e 100644 --- a/docker/env/base-env-docker-compose.yml +++ b/docker/env/base-env-docker-compose.yml @@ -53,7 +53,7 @@ services: environment: - "EMQX_NODE_NAME=emqx@node1.emqx.io" - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" - - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" + - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io]" healthcheck: test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] interval: 5s diff --git a/go-iot-mq/main.go b/go-iot-mq/main.go index b12bd2c..565f716 100644 --- a/go-iot-mq/main.go +++ b/go-iot-mq/main.go @@ -50,6 +50,7 @@ func main() { } zap.S().Infof("消息队列类型 %s", globalConfig.NodeInfo.Type) + CreateRabbitQueue("calc_queue") CreateRabbitQueue("waring_handler") CreateRabbitQueue("waring_notice") CreateRabbitQueue("transmit_handler") -- Gitee From 11330013bea7f683c6e61774d5a895009686fb6b Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 13:40:14 +0800 Subject: [PATCH 14/90] =?UTF-8?q?fix:IotGoProject.Dockerfile=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/IotGoProject.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/IotGoProject.Dockerfile b/deploy/IotGoProject.Dockerfile index 1968119..52094df 100644 --- a/deploy/IotGoProject.Dockerfile +++ b/deploy/IotGoProject.Dockerfile @@ -11,7 +11,7 @@ COPY ../notice ./notice COPY ../transmit ./transmit -RUN cd iot-go-project && go mod tidy && swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . +RUN cd iot-go-project && go get -u github.com/swaggo/swag/cmd/swag && swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . RUN chmod +x /app/iot-go-project/main # 运行阶段指定 scratch 作为基础镜像 -- Gitee From 1e6b935729bae789274e26d65be9018ddf647c86 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 13:50:58 +0800 Subject: [PATCH 15/90] =?UTF-8?q?fix:=E4=B8=B4=E6=97=B6=E5=B1=8F=E8=94=BDj?= =?UTF-8?q?wt=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iot-go-project/initialize/init.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index 01f4a51..ad8f590 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -418,7 +418,8 @@ func initLog() { } func initRouter(r *gin.RouterGroup) { - r.Use(router.JwtCheck()) + // todo 暂时屏蔽 + //r.Use(router.JwtCheck()) r.GET("/p/metrics", gin.WrapH(promhttp.Handler())) r.POST("/mqtt/create", mqttApi.CreateMqtt) r.GET("/mqtt/page", mqttApi.PageMqtt) -- Gitee From 278b43b8a88a757cbd0f3629ccf81e8cd0af99fa Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 13:55:29 +0800 Subject: [PATCH 16/90] =?UTF-8?q?fix:app=E7=9A=84docker-compose=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index 007578e..64e0435 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -83,6 +83,7 @@ services: image: go-iot-project:latest volumes: - ./iot-project/config/app-local.yml:/app/app-local.yml + - ./iot-project/fileupdate:/app/fileupdate ports: - 8005:8080 networks: -- Gitee From 780d46f8a6f11017e077e85db819177d4966713a Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 14:02:28 +0800 Subject: [PATCH 17/90] =?UTF-8?q?fix:IotGoProject.Dockerfile=E4=B8=B4?= =?UTF-8?q?=E6=97=B6=E6=B3=A8=E9=87=8Aswag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/IotGoProject.Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/IotGoProject.Dockerfile b/deploy/IotGoProject.Dockerfile index 52094df..32a581a 100644 --- a/deploy/IotGoProject.Dockerfile +++ b/deploy/IotGoProject.Dockerfile @@ -11,7 +11,8 @@ COPY ../notice ./notice COPY ../transmit ./transmit -RUN cd iot-go-project && go get -u github.com/swaggo/swag/cmd/swag && swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . +#RUN cd iot-go-project && go get -u github.com/swaggo/swag/cmd/swag && $GOPATH/bin/swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . +RUN cd iot-go-project && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . RUN chmod +x /app/iot-go-project/main # 运行阶段指定 scratch 作为基础镜像 -- Gitee From ea43d6d3ba8301315728fa1004f3faa852293728 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 15:12:39 +0800 Subject: [PATCH 18/90] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9mqtt=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E8=8A=82?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E4=B8=8E=E5=AE=BF=E4=B8=BB=E6=9C=BA=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app/mqtt/config/app-local.yml | 2 +- docker/app/mqtt/config/app-local2.yml | 2 +- docker/app/mqtt/config/app-local3.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/app/mqtt/config/app-local.yml b/docker/app/mqtt/config/app-local.yml index 1a57e27..2351dcd 100644 --- a/docker/app/mqtt/config/app-local.yml +++ b/docker/app/mqtt/config/app-local.yml @@ -1,6 +1,6 @@ node_info: host: 172.17.0.1 - port: 8081 + port: 8006 name: m1 type: mqtt size: 3 diff --git a/docker/app/mqtt/config/app-local2.yml b/docker/app/mqtt/config/app-local2.yml index 9bf6f25..6d7c78b 100644 --- a/docker/app/mqtt/config/app-local2.yml +++ b/docker/app/mqtt/config/app-local2.yml @@ -1,6 +1,6 @@ node_info: host: 172.17.0.1 - port: 8082 + port: 8007 name: m2 type: mqtt size: 3 diff --git a/docker/app/mqtt/config/app-local3.yml b/docker/app/mqtt/config/app-local3.yml index eeebc40..04b27de 100644 --- a/docker/app/mqtt/config/app-local3.yml +++ b/docker/app/mqtt/config/app-local3.yml @@ -1,6 +1,6 @@ node_info: host: 172.17.0.1 - port: 8081 + port: 8008 name: m3 type: mqtt size: 3 -- Gitee From 2cb77bd2ab16bd0e62f9f5320d11a11117413c1c Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 15:32:40 +0800 Subject: [PATCH 19/90] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9app=E7=9A=84docker-?= =?UTF-8?q?compose=E4=B8=ADmqtt=E7=AE=A1=E7=90=86=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app/docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index 64e0435..8ab4141 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -8,13 +8,13 @@ services: volumes: - ./mqtt/config/app-local.yml:/app/app-local.yml ports: - - 8006:8081 + - 8006:8006 networks: - iot-net iotgomqtt2: image: go-iot-mqtt:latest ports: - - 8007:8081 + - 8007:8007 entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] volumes: - ./mqtt/config/app-local2.yml:/app/app-local2.yml @@ -23,7 +23,7 @@ services: iotgomqtt3: image: go-iot-mqtt:latest ports: - - 8008:8081 + - 8008:8008 entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] volumes: - ./mqtt/config/app-local3.yml:/app/app-local3.yml -- Gitee From 8315de53f21e7eb5c1872bff34595749c85bc747 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 15:53:22 +0800 Subject: [PATCH 20/90] =?UTF-8?q?fix:=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6influxdb=E9=85=8D=E7=BD=AE=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app/iot-project/config/app-local.yml | 4 ++-- docker/app/mq/config/app-local-calc.yml | 4 ++-- docker/app/mq/config/app-local-pre_handler.yml | 4 ++-- docker/app/mq/config/app-local-waring_handler.yml | 4 ++-- docker/app/mq/config/app-local-wd.yml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/app/iot-project/config/app-local.yml b/docker/app/iot-project/config/app-local.yml index 11a2d93..7b396e4 100644 --- a/docker/app/iot-project/config/app-local.yml +++ b/docker/app/iot-project/config/app-local.yml @@ -14,9 +14,9 @@ mq_config: influx_config: host: 172.17.0.1 port: 8086 - token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + token: mytoken org: myorg - bucket: buc + bucket: mybucket mysql_config: username: app diff --git a/docker/app/mq/config/app-local-calc.yml b/docker/app/mq/config/app-local-calc.yml index 09d4447..2e895ee 100644 --- a/docker/app/mq/config/app-local-calc.yml +++ b/docker/app/mq/config/app-local-calc.yml @@ -21,9 +21,9 @@ mq_config: influx_config: host: 172.17.0.1 port: 8086 - token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + token: mytoken org: myorg - bucket: buc + bucket: mybucket mongo_config: host: 172.17.0.1 port: 27017 diff --git a/docker/app/mq/config/app-local-pre_handler.yml b/docker/app/mq/config/app-local-pre_handler.yml index bdb8df1..9ee3bdc 100644 --- a/docker/app/mq/config/app-local-pre_handler.yml +++ b/docker/app/mq/config/app-local-pre_handler.yml @@ -20,9 +20,9 @@ mq_config: influx_config: host: 172.17.0.1 port: 8086 - token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + token: mytoken org: myorg - bucket: buc + bucket: mybucket mongo_config: host: 172.17.0.1 port: 27017 diff --git a/docker/app/mq/config/app-local-waring_handler.yml b/docker/app/mq/config/app-local-waring_handler.yml index 0355d73..dddf354 100644 --- a/docker/app/mq/config/app-local-waring_handler.yml +++ b/docker/app/mq/config/app-local-waring_handler.yml @@ -20,9 +20,9 @@ mq_config: influx_config: host: 172.17.0.1 port: 8086 - token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + token: mytoken org: myorg - bucket: buc + bucket: mybucket mongo_config: host: 172.17.0.1 port: 27017 diff --git a/docker/app/mq/config/app-local-wd.yml b/docker/app/mq/config/app-local-wd.yml index bb2335b..c1e4441 100644 --- a/docker/app/mq/config/app-local-wd.yml +++ b/docker/app/mq/config/app-local-wd.yml @@ -20,9 +20,9 @@ mq_config: influx_config: host: 172.17.0.1 port: 8086 - token: i6XHSnNXeUoU3GoFXMm4qqrrgt69JKvQLqm0FCtnYG-rjb-nkDcry0pdwv4fpcXsSwi-mTGmAUTygkJtR-6CWA== + token: mytoken org: myorg - bucket: buc + bucket: mybucket mongo_config: host: 172.17.0.1 port: 27017 -- Gitee From 9ba4b687dcd296d516206fcebd4678739a28fa24 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 21:18:57 +0800 Subject: [PATCH 21/90] =?UTF-8?q?fix:IotGoProject.Dockerfile=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0swag=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/IotGoProject.Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deploy/IotGoProject.Dockerfile b/deploy/IotGoProject.Dockerfile index 32a581a..6999c8b 100644 --- a/deploy/IotGoProject.Dockerfile +++ b/deploy/IotGoProject.Dockerfile @@ -10,9 +10,7 @@ COPY ../iot-go-project ./iot-go-project COPY ../notice ./notice COPY ../transmit ./transmit - -#RUN cd iot-go-project && go get -u github.com/swaggo/swag/cmd/swag && $GOPATH/bin/swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . -RUN cd iot-go-project && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . +RUN cd iot-go-project && go install github.com/swaggo/swag/cmd/swag@latest && swag init --parseDependency --parseInternal --parseDepth 5 --instanceName "swagger" && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main . RUN chmod +x /app/iot-go-project/main # 运行阶段指定 scratch 作为基础镜像 -- Gitee From d9b6c837ea25591df2b3b7a677f9f24b6bcbab36 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 21:28:46 +0800 Subject: [PATCH 22/90] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index e6e5720..67f0477 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -92,6 +92,7 @@ func HandlerDataStorageString(d amqp.Delivery) { // 无 func StorageDataRowList(dt DataRowList) { signal2 := GetMqttClientSignal2(dt.DeviceUid) + zap.S().Infof("获取的mqtt信号数据signal2: %+v", signal2) timeFromUnix := time.Unix(dt.Time, 0) p := influxdb2.NewPointWithMeasurement(dt.DeviceUid). AddField("storage_time", time.Now().Unix()). @@ -112,7 +113,7 @@ func StorageDataRowList(dt DataRowList) { if signal2[row.Name].CacheSize > 0 { // 获取当前 ZSet 的大小 currentSize := globalRedisClient.ZCard(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID)).Val() - + zap.S().Infof("当前signal_delay_warning的大小: %+v", currentSize) // 如果 ZSet 的大小已经达到或超过配置的缓存大小,则移除第一个元素 if currentSize >= signal2[row.Name].CacheSize { // 移除 ZSet 中分数最低的元素,即最早的元素 -- Gitee From 51be88382c519639c01bfe2eb0399fe675d256ce Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 21:41:38 +0800 Subject: [PATCH 23/90] =?UTF-8?q?fix:=E5=89=8D=E7=AB=AFdocker=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6base=5Furl=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/.env.docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ant-vue/.env.docker b/ant-vue/.env.docker index 122f11c..cbec913 100644 --- a/ant-vue/.env.docker +++ b/ant-vue/.env.docker @@ -1,6 +1,6 @@ NODE_ENV=docker # 静态文件路径 -VITE_BASE_URL= +VITE_BASE_URL=/ VITE_APP_ENV_NAME=docker环境 VITE_APP_API_URL=http://172.17.0.1:8005 VITE_LOGIN=some -- Gitee From 2498805c385dcbb705f78eb0f1ca2fb5826824d6 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 22:01:00 +0800 Subject: [PATCH 24/90] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index 67f0477..52c8f4f 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -93,6 +93,7 @@ func HandlerDataStorageString(d amqp.Delivery) { func StorageDataRowList(dt DataRowList) { signal2 := GetMqttClientSignal2(dt.DeviceUid) zap.S().Infof("获取的mqtt信号数据signal2: %+v", signal2) + zap.S().Infof("当前的DataRowList数据: %+v", dt) timeFromUnix := time.Unix(dt.Time, 0) p := influxdb2.NewPointWithMeasurement(dt.DeviceUid). AddField("storage_time", time.Now().Unix()). @@ -116,25 +117,30 @@ func StorageDataRowList(dt DataRowList) { zap.S().Infof("当前signal_delay_warning的大小: %+v", currentSize) // 如果 ZSet 的大小已经达到或超过配置的缓存大小,则移除第一个元素 if currentSize >= signal2[row.Name].CacheSize { + zap.S().Infof("当前signal_delay_warning的currentSize大于等于配置大小:%+v", signal2[row.Name].CacheSize) // 移除 ZSet 中分数最低的元素,即最早的元素 i := signal2[row.Name].CacheSize + 1 - currentSize + zap.S().Infof("计算后的i: %+v", i) if i == 1 { - + zap.S().Infof("计算后的i的值为1") } else { + zap.S().Infof("开始移除之前的元素") err := globalRedisClient.ZRemRangeByRank(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), 0, i).Err() if err != nil { // 处理错误 zap.S().Errorf("移除 ZSet 元素异常:%+v", err) } } + } else { + zap.S().Infof("当前大小未超过配置大小,写入缓存") + // 写入缓存 + err := globalRedisClient.ZAdd(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), redis.Z{Score: float64(dt.Time), Member: row.Value}).Err() + if err != nil { + // 处理错误 + zap.S().Errorf("写入 ZSet 元素异常:%+v", err) + } } - // 写入缓存 - err := globalRedisClient.ZAdd(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), redis.Z{Score: float64(dt.Time), Member: row.Value}).Err() - if err != nil { - // 处理错误 - zap.S().Errorf("写入 ZSet 元素异常:%+v", err) - } } } -- Gitee From 9ebdade51acd4b0db064feb267f34a214695b656 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 22:08:09 +0800 Subject: [PATCH 25/90] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index 52c8f4f..4b0f6eb 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -110,7 +110,7 @@ func StorageDataRowList(dt DataRowList) { p.AddField(strconv.Itoa(signal2[row.Name].ID), row.Value) } - + zap.S().Infof("当前信号的的CacheSize:%+v", signal2[row.Name].CacheSize) if signal2[row.Name].CacheSize > 0 { // 获取当前 ZSet 的大小 currentSize := globalRedisClient.ZCard(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID)).Val() -- Gitee From e3fea8557e29fdf23834a4566f4746ef961fc65c Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 22:30:45 +0800 Subject: [PATCH 26/90] =?UTF-8?q?fix:ZRemRangeByRank=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index 4b0f6eb..d89b82c 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -125,7 +125,7 @@ func StorageDataRowList(dt DataRowList) { zap.S().Infof("计算后的i的值为1") } else { zap.S().Infof("开始移除之前的元素") - err := globalRedisClient.ZRemRangeByRank(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), 0, i).Err() + err := globalRedisClient.ZRemRangeByRank(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), 0, i-1).Err() if err != nil { // 处理错误 zap.S().Errorf("移除 ZSet 元素异常:%+v", err) -- Gitee From 04b0d1c74d2b6113aa1acacf1bd4a84cbc84b60e Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 23:03:41 +0800 Subject: [PATCH 27/90] =?UTF-8?q?fix:=E5=89=8D=E7=AB=AF=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=B7=B3=E8=BD=AC=E8=B7=AF=E7=94=B1=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/src/views/mqtt-management/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ant-vue/src/views/mqtt-management/index.vue b/ant-vue/src/views/mqtt-management/index.vue index fbc744c..d6e7f37 100644 --- a/ant-vue/src/views/mqtt-management/index.vue +++ b/ant-vue/src/views/mqtt-management/index.vue @@ -364,8 +364,8 @@ const cancel = (key: string) => { }; const onSignal = (id: string) => { - routerStore.setRouterName("/signal-configuration"); - jump.routeJump({ path: "/signal-configuration", query: { mqtt_client_id: id } }); + routerStore.setRouterName("/signal-configuration/index"); + jump.routeJump({ path: "/signal-configuration/index", query: { mqtt_client_id: id } }); }; const save = async (key: string) => { Object.assign(list.value.filter((item) => key === item.key)[0], editableData[key]); -- Gitee From ff54c607da992d887decdc080a95b1351f99b27a Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Wed, 24 Jul 2024 23:06:17 +0800 Subject: [PATCH 28/90] =?UTF-8?q?feat:=E6=95=B0=E6=8D=AE=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_storage.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go-iot-mq/handler_storage.go b/go-iot-mq/handler_storage.go index d89b82c..2f44401 100644 --- a/go-iot-mq/handler_storage.go +++ b/go-iot-mq/handler_storage.go @@ -110,7 +110,7 @@ func StorageDataRowList(dt DataRowList) { p.AddField(strconv.Itoa(signal2[row.Name].ID), row.Value) } - zap.S().Infof("当前信号的的CacheSize:%+v", signal2[row.Name].CacheSize) + zap.S().Infof("当前信号的的CacheSize:%+v=============rowName:%+v", signal2[row.Name].CacheSize, row.Name) if signal2[row.Name].CacheSize > 0 { // 获取当前 ZSet 的大小 currentSize := globalRedisClient.ZCard(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID)).Val() @@ -134,6 +134,7 @@ func StorageDataRowList(dt DataRowList) { } else { zap.S().Infof("当前大小未超过配置大小,写入缓存") // 写入缓存 + // 根据zset的特效,如果value一致的话,则会修改score,此处体现为修改了该值的时间,也就是说最新的值和之前的值相同的话只会保留最新时间的这一份 err := globalRedisClient.ZAdd(context.Background(), "signal_delay_warning:"+dt.DeviceUid+":"+strconv.Itoa(signal2[row.Name].ID), redis.Z{Score: float64(dt.Time), Member: row.Value}).Err() if err != nil { // 处理错误 -- Gitee From 2ecde9ba224637b25a67fc8a1c9d55c593cfa1b7 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 00:14:41 +0800 Subject: [PATCH 29/90] =?UTF-8?q?feat:mogodb=E5=A2=9E=E5=8A=A0=E6=97=B6?= =?UTF-8?q?=E5=8C=BA,=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app-start.sh | 5 +++++ docker/env-start.sh | 3 +++ docker/env/base-env-docker-compose.yml | 2 ++ docker/env/mongo/init-mongo.js | 22 ++++++++++++++++++++++ docker/start.sh | 3 --- 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 docker/app-start.sh create mode 100644 docker/env-start.sh create mode 100644 docker/env/mongo/init-mongo.js delete mode 100644 docker/start.sh diff --git a/docker/app-start.sh b/docker/app-start.sh new file mode 100644 index 0000000..af54de3 --- /dev/null +++ b/docker/app-start.sh @@ -0,0 +1,5 @@ +#!/bin/zsh +docker-compose -f ./app/docker-compose.yml down +docker rmi go-iot-project:latest go-iot-admin-vue:latest go-iot-mq:latest go-iot-mqtt:latest +docker-compose -f ./app/docker-compose.yml up -d +echo "项目启动中..." diff --git a/docker/env-start.sh b/docker/env-start.sh new file mode 100644 index 0000000..f8c2634 --- /dev/null +++ b/docker/env-start.sh @@ -0,0 +1,3 @@ +#!bin/bash +docker-compose -f ./env/base-env-docker-compose.yml up -d +echo "环境后台准备中...,请稍后" diff --git a/docker/env/base-env-docker-compose.yml b/docker/env/base-env-docker-compose.yml index 7a49f8e..658feab 100644 --- a/docker/env/base-env-docker-compose.yml +++ b/docker/env/base-env-docker-compose.yml @@ -30,9 +30,11 @@ services: - 27017:27017 volumes: - ./mongo/database:/data/db + - ./mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js environment: - MONGO_INITDB_ROOT_USERNAME=admin - MONGO_INITDB_ROOT_PASSWORD=admin + - TZ=Asia/Shanghai networks: - iot-net mongo-express: diff --git a/docker/env/mongo/init-mongo.js b/docker/env/mongo/init-mongo.js new file mode 100644 index 0000000..3617393 --- /dev/null +++ b/docker/env/mongo/init-mongo.js @@ -0,0 +1,22 @@ +// dbAdmin = db.getSiblingDB("admin"); +// dbAdmin.createUser({ +// user: "iot", +// pwd: "iot123", +// roles: [{ role: "userAdminAnyDatabase", db: "admin" }], +// mechanisms: ["SCRAM-SHA-1"], +// }); +// +// // Authenticate user +// dbAdmin.auth({ +// user: "iot", +// pwd: "iot123", +// mechanisms: ["SCRAM-SHA-1"], +// digestPassword: true, +// }); + +use iot; +db.createCollection("calc"); +db.createCollection("waring"); +db.createCollection("script_waring"); + +// todo 导入数据可以在此处操作,或者新建一个单独的用户来操作有权限的库 diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100644 index 1d9108d..0000000 --- a/docker/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/zsh -docker compose -f ./env/base-env-docker-compose.yml -f ./app/docker-compose.yml up -d -echo "执行完毕,项目启动中..." -- Gitee From 22b8634ed71092dba231945b0fe1473a2507c371 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 09:07:33 +0800 Subject: [PATCH 30/90] =?UTF-8?q?fix:docker-compose=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=AE=B9=E5=99=A8=E5=86=85=E6=97=B6=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/app-start.sh | 2 +- docker/app/docker-compose.yml | 18 ++++++++++++++++++ docker/env/base-env-docker-compose.yml | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docker/app-start.sh b/docker/app-start.sh index af54de3..36048d9 100644 --- a/docker/app-start.sh +++ b/docker/app-start.sh @@ -2,4 +2,4 @@ docker-compose -f ./app/docker-compose.yml down docker rmi go-iot-project:latest go-iot-admin-vue:latest go-iot-mq:latest go-iot-mqtt:latest docker-compose -f ./app/docker-compose.yml up -d -echo "项目启动中..." +echo "项目已启动" diff --git a/docker/app/docker-compose.yml b/docker/app/docker-compose.yml index 8ab4141..8312c1a 100644 --- a/docker/app/docker-compose.yml +++ b/docker/app/docker-compose.yml @@ -5,6 +5,8 @@ services: dockerfile: deploy/IotMQTT.Dockerfile image: go-iot-mqtt:latest entrypoint: ["/app/main", "-config", "/app/app-local.yml"] + environment: + - TZ=Asia/Shanghai volumes: - ./mqtt/config/app-local.yml:/app/app-local.yml ports: @@ -16,6 +18,8 @@ services: ports: - 8007:8007 entrypoint: ["/app/main", "-config", "/app/app-local2.yml"] + environment: + - TZ=Asia/Shanghai volumes: - ./mqtt/config/app-local2.yml:/app/app-local2.yml networks: @@ -25,6 +29,8 @@ services: ports: - 8008:8008 entrypoint: ["/app/main", "-config", "/app/app-local3.yml"] + environment: + - TZ=Asia/Shanghai volumes: - ./mqtt/config/app-local3.yml:/app/app-local3.yml networks: @@ -39,6 +45,8 @@ services: volumes: - ./mq/config/app-local-pre_handler.yml:/app/app-local-pre_handler.yml entrypoint: ["/app/main", "-config", "/app/app-local-pre_handler.yml"] + environment: + - TZ=Asia/Shanghai depends_on: - iotgomqtt1 networks: @@ -50,6 +58,8 @@ services: volumes: - ./mq/config/app-local-calc.yml:/app/app-local-calc.yml entrypoint: ["/app/main", "-config", "/app/app-local-calc.yml"] + environment: + - TZ=Asia/Shanghai depends_on: - iotgomq-pre_handler networks: @@ -61,6 +71,8 @@ services: volumes: - ./mq/config/app-local-waring_handler.yml:/app/app-local-waring_handler.yml entrypoint: ["/app/main", "-config", "/app/app-local-waring_handler.yml"] + environment: + - TZ=Asia/Shanghai depends_on: - iotgomq-calc_handler networks: @@ -72,6 +84,8 @@ services: volumes: - ./mq/config/app-local-wd.yml:/app/app-local-wd.yml entrypoint: ["/app/main", "-config", "/app/app-local-wd.yml"] + environment: + - TZ=Asia/Shanghai depends_on: - iotgomq-calc_handler networks: @@ -84,6 +98,8 @@ services: volumes: - ./iot-project/config/app-local.yml:/app/app-local.yml - ./iot-project/fileupdate:/app/fileupdate + environment: + - TZ=Asia/Shanghai ports: - 8005:8080 networks: @@ -93,6 +109,8 @@ services: context: ../../ dockerfile: deploy/IotAdminVue.Dockerfile image: go-iot-admin-vue:latest + environment: + - TZ=Asia/Shanghai ports: - 8080:80 networks: diff --git a/docker/env/base-env-docker-compose.yml b/docker/env/base-env-docker-compose.yml index 658feab..9bd25f2 100644 --- a/docker/env/base-env-docker-compose.yml +++ b/docker/env/base-env-docker-compose.yml @@ -8,6 +8,8 @@ services: volumes: # Mount for influxdb data directory and configuration - influxdbv2:/var/lib/influxdb2:rw + environment: + - TZ=Asia/Shanghai ports: - "8086:8086" networks: @@ -19,6 +21,8 @@ services: volumes: # Mount for telegraf config - ./influx/telegraf/mytelegraf.conf:/etc/telegraf/telegraf.conf:ro + environment: + - TZ=Asia/Shanghai env_file: - ./influx/influxv2.env networks: @@ -47,6 +51,7 @@ services: - ME_CONFIG_MONGODB_ADMINUSERNAME=admin - ME_CONFIG_MONGODB_ADMINPASSWORD=admin - ME_CONFIG_MONGODB_SERVER=mongodb + - TZ=Asia/Shanghai networks: - iot-net emqx1: @@ -56,6 +61,7 @@ services: - "EMQX_NODE_NAME=emqx@node1.emqx.io" - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io]" + - TZ=Asia/Shanghai healthcheck: test: [ "CMD", "/opt/emqx/bin/emqx ctl", "status" ] interval: 5s @@ -101,6 +107,7 @@ services: - ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:rw environment: - RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/usr/lib/rabbitmq/plugins + - TZ=Asia/Shanghai networks: - iot-net @@ -110,6 +117,8 @@ services: ports: - '6379:6379' command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + environment: + - TZ=Asia/Shanghai volumes: - ./redis/data:/data networks: -- Gitee From b5de305d69ef47393f49414036f0c5e68c911bc1 Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 09:17:22 +0800 Subject: [PATCH 31/90] =?UTF-8?q?fix:=E9=A1=B5=E9=9D=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/src/views/visualization/add.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ant-vue/src/views/visualization/add.vue b/ant-vue/src/views/visualization/add.vue index 5b5c7e4..b8d8c8a 100644 --- a/ant-vue/src/views/visualization/add.vue +++ b/ant-vue/src/views/visualization/add.vue @@ -327,7 +327,7 @@ const onConfirm = () => { }; if (activeKey.value === "dynamic_Time") { listArr.value[indexNumber.value].param.sub = activeKey.value === "dynamic_Time" ? dateTime.value : ""; - listArr.value[indexNumber.value].param.sub = activeKey.value === "dynamic_Time" ? dateUnit.value : ""; + listArr.value[indexNumber.value].param.dateUnit = activeKey.value === "dynamic_Time" ? dateUnit.value : ""; } if (activeKey.value === "static_Time") { listArr.value[indexNumber.value].param.start_time = activeKey.value === "dynamic_Time" ? start_time : form.start_time; @@ -418,8 +418,7 @@ const onSaveInformation = () => { const list = listArr.value.map((it) => ({ name: it.name, id: it.id, show: it.show, param: it.param, showSpinning: it.showSpinning })); const data = { config: JSON.stringify(list), - name: createName.value, - id:"" + name: createName.value }; if (route.query.id) { data.id = id.value; -- Gitee From 10472249f01ca95f3f4618e8c08725c795f700eb Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 09:29:16 +0800 Subject: [PATCH 32/90] =?UTF-8?q?fix:=E9=A1=B5=E9=9D=A2=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E8=B7=AF=E7=94=B1path=E5=92=8CRouter=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/src/views/calculation-rules/index.vue | 4 ++-- ant-vue/src/views/script-alarm/index.vue | 4 ++-- ant-vue/src/views/signal-configuration/index.vue | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ant-vue/src/views/calculation-rules/index.vue b/ant-vue/src/views/calculation-rules/index.vue index bf2ace9..07be2a7 100644 --- a/ant-vue/src/views/calculation-rules/index.vue +++ b/ant-vue/src/views/calculation-rules/index.vue @@ -363,8 +363,8 @@ const onAddData = () => ; const onGo = (id: string) => { - routerStore.setRouterName("/calculate-parameters"); - jump.routeJump({ path: "/calculate-parameters", query: { rule_id: id } }); + routerStore.setRouterName("/calculate-parameters/index"); + jump.routeJump({ path: "/calculate-parameters/index", query: { rule_id: id } }); }; const handleCancel = () => { modalVisible.value = false; diff --git a/ant-vue/src/views/script-alarm/index.vue b/ant-vue/src/views/script-alarm/index.vue index e4157c2..2b82906 100644 --- a/ant-vue/src/views/script-alarm/index.vue +++ b/ant-vue/src/views/script-alarm/index.vue @@ -327,8 +327,8 @@ const onAddUpdateData = () => { }; const onGo = (id: string) => { - routerStore.setRouterName("/script-alarm-parameters"); - jump.routeJump({ path: "/script-alarm-parameters", query: { signal_delay_waring_id: id } }); + routerStore.setRouterName("/script-alarm-parameters/index"); + jump.routeJump({ path: "/script-alarm-parameters/index", query: { signal_delay_waring_id: id } }); }; const handleCancel = () => { modalVisible.value = false; diff --git a/ant-vue/src/views/signal-configuration/index.vue b/ant-vue/src/views/signal-configuration/index.vue index 779e88c..7fc2187 100644 --- a/ant-vue/src/views/signal-configuration/index.vue +++ b/ant-vue/src/views/signal-configuration/index.vue @@ -293,8 +293,8 @@ const pageList = async () => { })); }; const onSignal = (id: string, mqtt_client_id: string) => { - routerStore.setRouterName("/signal"); - jump.routeJump({ path: "/signal", query: { id, mqtt_client_id } }); + routerStore.setRouterName("/signal/index"); + jump.routeJump({ path: "/signal/index", query: { id, mqtt_client_id } }); }; const handleTableChange = async (page: any) => { -- Gitee From a66d007561e4e90248d968f478d0906c6214584f Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 09:39:48 +0800 Subject: [PATCH 33/90] =?UTF-8?q?fix:getDelayScript=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_waring_delay.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/go-iot-mq/handler_waring_delay.go b/go-iot-mq/handler_waring_delay.go index 5a8d24f..2b3d22d 100644 --- a/go-iot-mq/handler_waring_delay.go +++ b/go-iot-mq/handler_waring_delay.go @@ -58,6 +58,7 @@ func handlerWaringDelayOnce(msg DataRowList) { zap.S().Infof("处理 handlerWaringDelayOnce 数据: %+v", msg) uid := msg.DeviceUid mapping := getDelayParam(uid, msg.DataRows) + zap.S().Infof("getDelayParam 数据: %+v", mapping) background := context.Background() var scriptParam = make(map[string][]Tv) for _, param := range mapping { @@ -74,6 +75,7 @@ func handlerWaringDelayOnce(msg DataRowList) { } script := getDelayScript(mapping) + zap.S().Infof("getDelayScript结果: %+v", script) zap.S().Infof("脚本报警参数 = %+v", scriptParam) db := GMongoClient.Database(globalConfig.MongoConfig.Db) collection := db.Collection(globalConfig.MongoConfig.ScriptWaringCollection) @@ -81,6 +83,7 @@ func handlerWaringDelayOnce(msg DataRowList) { for _, waring := range script { zap.S().Infof("key = %+v", waring) delayScript := runWaringDelayScript(waring.Script, scriptParam) + zap.S().Infof("runWaringDelayScript 执行后数据: %+v", delayScript) toInsert = append(toInsert, bson.M{ "device_uid": uid, "param": scriptParam, @@ -91,6 +94,7 @@ func handlerWaringDelayOnce(msg DataRowList) { "up_time": msg.Time, }) } + zap.S().Infof("toInsert 数据: %+v", toInsert) if toInsert != nil { one, err := collection.InsertMany(context.Background(), toInsert) @@ -150,7 +154,19 @@ func getDelayScript(mapping []SignalDelayWaringParam) []SignalDelayWaring { } res = append(res, singw) } - return res + // 使用map来存储已经出现过的ID + idMap := make(map[int]bool) + + var uniqueRes []SignalDelayWaring + for _, item := range res { + if _, exists := idMap[item.ID]; !exists { + // 如果ID在map中不存在,则添加到结果数组中 + uniqueRes = append(uniqueRes, item) + // 将ID添加到map中,标记为已存在 + idMap[item.ID] = true + } + } + return uniqueRes } // getDelayParam 函数根据用户UID和DataRow切片从Redis中获取延迟报警参数 -- Gitee From f9a34213d8f54b6f88362a7fe0d2379b2b3580cc Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Thu, 25 Jul 2024 11:05:46 +0800 Subject: [PATCH 34/90] =?UTF-8?q?feat:=20http=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iot-go-project/biz/http_biz.go | 47 ++++ iot-go-project/initialize/init.go | 35 ++- iot-go-project/models/models.go | 16 ++ iot-go-project/router/device_info_router.go | 136 ++++++++++- iot-go-project/router/http_handler_router.go | 181 +++++++++++++++ iot-go-project/servlet/servlet.go | 4 + protocol/http/app-local.yml | 16 ++ protocol/http/go.mod | 8 +- protocol/http/go.sum | 18 ++ protocol/http/main.go | 223 ++++++++++++++++++- protocol/http/rabbit_mq.go | 81 +++++++ protocol/readme.md | 26 ++- protocol/readme/image-20240725103427236.png | Bin 0 -> 181129 bytes protocol/tcp/server.go | 2 +- 14 files changed, 771 insertions(+), 22 deletions(-) create mode 100644 iot-go-project/biz/http_biz.go create mode 100644 iot-go-project/router/http_handler_router.go create mode 100644 protocol/http/app-local.yml create mode 100644 protocol/http/rabbit_mq.go create mode 100644 protocol/readme/image-20240725103427236.png diff --git a/iot-go-project/biz/http_biz.go b/iot-go-project/biz/http_biz.go new file mode 100644 index 0000000..0659af2 --- /dev/null +++ b/iot-go-project/biz/http_biz.go @@ -0,0 +1,47 @@ +package biz + +import ( + "context" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type HttpHandlerBiz struct{} + +func (biz *HttpHandlerBiz) ById(id uint) (*models.HttpHandler, error) { + var HttpHandler models.HttpHandler + + result := glob.GDb.First(&HttpHandler, id) + if result.Error != nil { + return nil, result.Error + } + return &HttpHandler, nil +} + +func (biz *HttpHandlerBiz) PageData(name string, page, size int) (*servlet.PaginationQ, error) { + var pagination servlet.PaginationQ + var HttpHandlerList []models.HttpHandler + + db := glob.GDb + if name != "" { + db = db.Where("name LIKE ?", "%"+name+"%") + } + + db.Model(&models.HttpHandler{}).Count(&pagination.Total) // 计算总记录数 + offset := (page - 1) * size + db.Offset(offset).Limit(size).Find(&HttpHandlerList) + pagination.Data = HttpHandlerList + pagination.Page = page + pagination.Size = size + + return &pagination, nil +} + +func (biz *HttpHandlerBiz) SetRedis(data models.HttpHandler) { + glob.GRedis.HSet(context.Background(), "struct:Http", strconv.Itoa(int(data.ID)), data.Script) +} +func (biz *HttpHandlerBiz) RemoveRedis(data models.HttpHandler) { + glob.GRedis.HDel(context.Background(), "struct:Http", strconv.Itoa(int(data.ID))) +} diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index a7c5b8c..ff54545 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -63,7 +63,8 @@ var ( feishuApi = notice.FeiShuApi{} dingdingApi = notice.DingDingApi{} - tcpHandlerApi = router.TcpHandlerApi{} + tcpHandlerApi = router.TcpHandlerApi{} + httpHandlerApi = router.HttpHandlerApi{} ) func initTable() { @@ -343,6 +344,27 @@ func initTable() { zap.S().Errorf("数据库表创建失败 %+v", err) } } + if !glob.GDb.Migrator().HasTable(&models.DeviceBindTcpHandler{}) { + + err := glob.GDb.AutoMigrate(&models.DeviceBindTcpHandler{}) + if err != nil { + zap.S().Errorf("数据库表创建失败 %+v", err) + } + } + if !glob.GDb.Migrator().HasTable(&models.HttpHandler{}) { + + err := glob.GDb.AutoMigrate(&models.HttpHandler{}) + if err != nil { + zap.S().Errorf("数据库表创建失败 %+v", err) + } + } + if !glob.GDb.Migrator().HasTable(&models.DeviceBindHTTPHandler{}) { + + err := glob.GDb.AutoMigrate(&models.DeviceBindHTTPHandler{}) + if err != nil { + zap.S().Errorf("数据库表创建失败 %+v", err) + } + } } func initDb() { @@ -518,7 +540,10 @@ func initRouter(r *gin.RouterGroup) { r.POST("/DeviceInfo/delete/:id", deviceInfoApi.DeleteDeviceInfo) r.POST("/DeviceInfo/BindMqtt", deviceInfoApi.BindMqtt) r.POST("/DeviceInfo/BindTcp", deviceInfoApi.BindTcp) - r.POST("/DeviceInfo/QueryBindMqtt", deviceInfoApi.QueryBindMqtt) + r.POST("/DeviceInfo/BindHTTP", deviceInfoApi.BindHTTP) + r.GET("/DeviceInfo/QueryBindMqtt", deviceInfoApi.QueryBindMqtt) + r.GET("/DeviceInfo/QueryBindTcp", deviceInfoApi.QueryBindTcp) + r.GET("/DeviceInfo/QueryBindHTTP", deviceInfoApi.QueryBindHttp) r.POST("/ProductionPlan/create", productionPlanApi.CreateProductionPlan) r.POST("/ProductionPlan/update", productionPlanApi.UpdateProductionPlan) @@ -625,6 +650,12 @@ func initRouter(r *gin.RouterGroup) { r.GET("/TcpHandler/page", tcpHandlerApi.PageTcpHandler) r.POST("/TcpHandler/delete/:id", tcpHandlerApi.DeleteTcpHandler) + r.POST("/HttpHandler/create", httpHandlerApi.CreateHttpHandler) + r.POST("/HttpHandler/update", httpHandlerApi.UpdateHttpHandler) + r.GET("/HttpHandler/:id", httpHandlerApi.ByIdHttpHandler) + r.GET("/HttpHandler/page", httpHandlerApi.PageHttpHandler) + r.POST("/HttpHandler/delete/:id", httpHandlerApi.DeleteHttpHandler) + } func initGlobalRedisClient() { diff --git a/iot-go-project/models/models.go b/iot-go-project/models/models.go index 064938e..25f0040 100644 --- a/iot-go-project/models/models.go +++ b/iot-go-project/models/models.go @@ -250,6 +250,7 @@ type DeviceBindTcpHandler struct { TcpHandlerId uint `json:"tcp_handler_id" structs:"tcp_handler_id"` // TCP处理器的ID } + // TcpHandler 表示TCP数据处理器 type TcpHandler struct { gorm.Model `structs:"-"` @@ -257,6 +258,21 @@ type TcpHandler struct { Script string `json:"script" structs:"script"` // 处理器脚本 } +type DeviceBindHTTPHandler struct { + gorm.Model `structs:"-"` + DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID + HttpHandlerId uint `json:"http_handler_id" structs:"http_handler_id"` // HTTP处理器的ID +} + +type HttpHandler struct { + Name string `json:"name" structs:"name"` // 处理器名 + Username string `json:"username" structs:"username"` // 用户名 + Password string `json:"password" structs:"password"` // 密码 + Script string `json:"script" structs:"script"` // 脚本 + gorm.Model `structs:"-"` +} + + type DeviceGroupBindMqttClient struct { gorm.Model `structs:"-"` DeviceGroupId uint `json:"device_group_id" structs:"device_group_id"` // 设备组ID diff --git a/iot-go-project/router/device_info_router.go b/iot-go-project/router/device_info_router.go index fa617da..adcedeb 100644 --- a/iot-go-project/router/device_info_router.go +++ b/iot-go-project/router/device_info_router.go @@ -228,6 +228,49 @@ func (api *DeviceInfoApi) QueryBindMqtt(c *gin.Context) { servlet.Resp(c, deviceBindMqttClients) } +// QueryBindHttp +// @Tags DeviceInfos +// @Summary 查询绑定HTTP客户端 +// @Accept json +// @Produce json +// @Param device_info_id path int true "主键" +// @Router /DeviceInfo/QueryBindHTTP [get] +func (api *DeviceInfoApi) QueryBindHttp(c *gin.Context) { + param := c.Param("device_info_id") + + var res []models.DeviceBindHTTPHandler + + // 使用 Where 和 Find 方法查询记录 + result := glob.GDb.Where("`device_info_id` = ?", param).Find(&res) + if result.Error != nil { + zap.S().Infoln("Error occurred during query:", result.Error) + servlet.Error(c, "暂无数据") + return + } + servlet.Resp(c, res) +} +// QueryBindTcp +// @Tags DeviceInfos +// @Summary 查询绑定tcp客户端 +// @Accept json +// @Produce json +// @Param device_info_id path int true "主键" +// @Router /DeviceInfo/QueryBindTcp [get] +func (api *DeviceInfoApi) QueryBindTcp(c *gin.Context) { + param := c.Param("device_info_id") + + var res []models.DeviceBindTcpHandler + + // 使用 Where 和 Find 方法查询记录 + result := glob.GDb.Where("`device_info_id` = ?", param).Find(&res) + if result.Error != nil { + zap.S().Infoln("Error occurred during query:", result.Error) + servlet.Error(c, "暂无数据") + return + } + servlet.Resp(c, res) +} + // BindMqtt // @Tags DeviceInfos // @Summary 绑定mqtt客户端 @@ -300,7 +343,6 @@ func (api *DeviceInfoApi) BindMqtt(c *gin.Context) { glob.GRedis.LPush(context.Background(), "mqtt_client_id_bind_product:"+strconv.Itoa(item), DeviceInfo.ProductId) } - for _, client := range toDel { glob.GRedis.Del(context.Background(), "mqtt_client_id_bind_device_info:"+strconv.Itoa(int(client.MqttClientId))) } @@ -310,13 +352,10 @@ func (api *DeviceInfoApi) BindMqtt(c *gin.Context) { } - servlet.Resp(c, "绑定成功") } - - // BindTcp // @Tags DeviceInfos // @Summary 绑定tcp处理器 @@ -342,7 +381,7 @@ func (api *DeviceInfoApi) BindTcp(c *gin.Context) { tx.Where("`device_info_id` = ?", param.DeviceId).Find(toDel) - result := tx.Where("`device_info_id` = ?", param.DeviceId).Delete(&models.DeviceBindMqttClient{}) + result := tx.Where("`device_info_id` = ?", param.DeviceId).Delete(&models.DeviceBindTcpHandler{}) if result.Error != nil { // 如果出现错误,回滚事务 @@ -389,7 +428,6 @@ func (api *DeviceInfoApi) BindTcp(c *gin.Context) { glob.GRedis.LPush(context.Background(), "tcp_bind_product:"+strconv.Itoa(item), DeviceInfo.ProductId) } - for _, client := range toDel { glob.GRedis.Del(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(int(client.TcpHandlerId))) } @@ -399,7 +437,91 @@ func (api *DeviceInfoApi) BindTcp(c *gin.Context) { } + servlet.Resp(c, "绑定成功") + +} + +// BindHTTP +// @Tags DeviceInfos +// @Summary 绑定tcp处理器 +// @Accept json +// @Produce json +// @Param DeviceGroup body servlet.DeviceBindHTTPParam true "绑定参数" +// @Router /DeviceInfo/BindHTTP [post] +func (api *DeviceInfoApi) BindHTTP(c *gin.Context) { + var param servlet.DeviceBindHTTPParam + if err := c.ShouldBindJSON(¶m); err != nil { + + servlet.Error(c, err.Error()) + return + } + + // 开启事务 + tx := glob.GDb.Begin() + if tx.Error != nil { + servlet.Error(c, "Failed to begin transaction") + return + } + var toDel []models.DeviceBindHTTPHandler + + tx.Where("`device_info_id` = ?", param.DeviceId).Find(toDel) + + result := tx.Where("`device_info_id` = ?", param.DeviceId).Delete(&models.DeviceBindHTTPHandler{}) + + if result.Error != nil { + // 如果出现错误,回滚事务 + tx.Rollback() + servlet.Error(c, "Error occurred during deletion") + return + } + + var deviceBindHTTPHandlers []models.DeviceBindHTTPHandler + for _, httpId := range param.HttpHandlerId { + deviceBindHTTPHandlers = append(deviceBindHTTPHandlers, models.DeviceBindHTTPHandler{ + DeviceInfoId: uint(param.DeviceId), + HttpHandlerId: uint(httpId), + }) + } + + result = tx.Model(&models.DeviceBindHTTPHandler{}).CreateInBatches(deviceBindHTTPHandlers, len(deviceBindHTTPHandlers)) + if result.Error != nil { + tx.Rollback() + zap.S().Infoln("Error occurred during creation:", result.Error) + servlet.Error(c, "Error occurred during creation") + return + } + if err := tx.Commit().Error; err != nil { + servlet.Error(c, "Failed to commit transaction") + return + } + + // redis 中建立 tcp 与 device_info_id 的映射 + + var DeviceInfo models.DeviceInfo + + first := tx.First(&DeviceInfo, param.DeviceId) + if first.Error != nil { + servlet.Error(c, "DeviceInfo not found") + return + } + + for _, client := range toDel { + glob.GRedis.Del(context.Background(), "tcp_bind_product:"+strconv.Itoa(int(client.HttpHandlerId))) + } + + for _, item := range param.HttpHandlerId { + glob.GRedis.LPush(context.Background(), "tcp_bind_product:"+strconv.Itoa(item), DeviceInfo.ProductId) + } + + for _, client := range toDel { + glob.GRedis.Del(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(int(client.HttpHandlerId))) + } + + for _, item := range param.HttpHandlerId { + glob.GRedis.LPush(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(item), DeviceInfo.ID) + + } servlet.Resp(c, "绑定成功") -} \ No newline at end of file +} diff --git a/iot-go-project/router/http_handler_router.go b/iot-go-project/router/http_handler_router.go new file mode 100644 index 0000000..f57f701 --- /dev/null +++ b/iot-go-project/router/http_handler_router.go @@ -0,0 +1,181 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "igp/biz" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type HttpHandlerApi struct{} + +var HttpHandlerBiz = biz.HttpHandlerBiz{} + +// CreateHttpHandler +// @Summary 创建Http数据处理器 +// @Description 创建Http数据处理器 +// @Tags HttpHandlers +// @Accept json +// @Produce json +// @Param HttpHandler body models.HttpHandler true "Http数据处理器" +// @Success 201 {object} servlet.JSONResult{data=models.HttpHandler} "创建成功的Http数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 500 {string} string "内部服务器错误" +// @Router /HttpHandler/create [post] +func (api *HttpHandlerApi) CreateHttpHandler(c *gin.Context) { + var HttpHandler models.HttpHandler + if err := c.ShouldBindJSON(&HttpHandler); err != nil { + servlet.Error(c, err.Error()) + return + } + + // 检查 HttpHandler 是否被正确初始化 + if HttpHandler.Name == "" { + servlet.Error(c, "名称不能为空") + return + } + + result := glob.GDb.Create(&HttpHandler) + + if result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + HttpHandlerBiz.SetRedis(HttpHandler) + // 返回创建成功的Http数据处理器 + servlet.Resp(c, HttpHandler) +} + +// UpdateHttpHandler +// @Summary 更新一个Http数据处理器 +// @Description 更新一个Http数据处理器 +// @Tags HttpHandlers +// @Accept json +// @Produce json +// @Param HttpHandler body models.HttpHandler true "Http数据处理器" +// @Success 200 {object} servlet.JSONResult{data=models.HttpHandler} "Http数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 404 {string} string "Http数据处理器未找到" +// @Failure 500 {string} string "内部服务器错误" +// @Router /HttpHandler/update [post] +func (api *HttpHandlerApi) UpdateHttpHandler(c *gin.Context) { + var req models.HttpHandler + if err := c.ShouldBindJSON(&req); err != nil { + + servlet.Error(c, err.Error()) + return + } + + var old models.HttpHandler + result := glob.GDb.First(&old, req.ID) + if result.Error != nil { + + servlet.Error(c, "HttpHandler not found") + return + } + + var newV models.HttpHandler + newV = old + newV.Name = req.Name + newV.Script = req.Script + result = glob.GDb.Model(&newV).Updates(newV) + + if result.Error != nil { + + servlet.Error(c, result.Error.Error()) + return + } + HttpHandlerBiz.SetRedis(newV) + servlet.Resp(c, old) +} + +// PageHttpHandler +// @Summary 分页查询Http数据处理器 +// @Description 分页查询Http数据处理器 +// @Tags HttpHandlers +// @Accept json +// @Produce json +// @Param name query string false "Http数据处理器名称" +// @Param pid query int false "上级id" +// @Param page query int false "页码" default(0) +// @Param page_size query int false "每页大小" default(10) +// @Success 200 {object} servlet.JSONResult{data=servlet.PaginationQ{data=models.HttpHandler}} "Http数据处理器" +// @Failure 400 {string} string "请求参数错误" +// @Failure 500 {string} string "查询异常" +// @Router /HttpHandler/page [get] +func (api *HttpHandlerApi) PageHttpHandler(c *gin.Context) { + var name = c.Query("name") + var page = c.DefaultQuery("page", "0") + var pageSize = c.DefaultQuery("page_size", "10") + parseUint, err := strconv.Atoi(page) + if err != nil { + servlet.Error(c, "无效的页码") + return + } + u, err := strconv.Atoi(pageSize) + + if err != nil { + servlet.Error(c, "无效的页长") + return + } + + data, err := HttpHandlerBiz.PageData(name, parseUint, u) + if err != nil { + servlet.Error(c, "查询异常") + return + } + servlet.Resp(c, data) +} + +// DeleteHttpHandler +// @Tags HttpHandlers +// @Summary 删除Http数据处理器 +// @Produce application/json +// @Param id path int true "主键" +// @Router /HttpHandler/delete/:id [post] +func (api *HttpHandlerApi) DeleteHttpHandler(c *gin.Context) { + var HttpHandler models.HttpHandler + + param := c.Param("id") + + result := glob.GDb.First(&HttpHandler, param) + if result.Error != nil { + servlet.Error(c, "HttpHandler not found") + + return + } + + if result := glob.GDb.Delete(&HttpHandler); result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + HttpHandlerBiz.RemoveRedis(HttpHandler) + + servlet.Resp(c, "删除成功") +} + +// ByIdHttpHandler +// @Tags HttpHandlers +// @Summary 单个详情 +// @Param id path int true "主键" +// @Produce application/json +// @Router /HttpHandler/:id [get] +func (api *HttpHandlerApi) ByIdHttpHandler(c *gin.Context) { + var HttpHandler models.HttpHandler + + param := c.Param("id") + + result := glob.GDb.First(&HttpHandler, param) + if result.Error != nil { + servlet.Error(c, "HttpHandler not found") + + return + } + + servlet.Resp(c, HttpHandler) +} + + + diff --git a/iot-go-project/servlet/servlet.go b/iot-go-project/servlet/servlet.go index c0cb72a..1df70c1 100644 --- a/iot-go-project/servlet/servlet.go +++ b/iot-go-project/servlet/servlet.go @@ -291,6 +291,10 @@ type DeviceBindTcpParam struct { DeviceId int `json:"device_id"` TcpHandlerId []int `json:"tcp_handler_id"` } +type DeviceBindHTTPParam struct { + DeviceId int `json:"device_id"` + HttpHandlerId []int `json:"http_handler_id"` +} type DeviceGroupBindMqttClientParam struct { diff --git a/protocol/http/app-local.yml b/protocol/http/app-local.yml new file mode 100644 index 0000000..42428fa --- /dev/null +++ b/protocol/http/app-local.yml @@ -0,0 +1,16 @@ +node_info: + host: 127.0.0.1 + port: 8888 +redis_config: + host: 127.0.0.1 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest \ No newline at end of file diff --git a/protocol/http/go.mod b/protocol/http/go.mod index 98b5312..229de3e 100644 --- a/protocol/http/go.mod +++ b/protocol/http/go.mod @@ -2,6 +2,12 @@ module iot-http go 1.22.4 +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/rabbitmq/amqp091-go v1.10.0 + go.uber.org/zap v1.27.0 +) + require ( github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect @@ -9,7 +15,6 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect @@ -23,6 +28,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect diff --git a/protocol/http/go.sum b/protocol/http/go.sum index 3c995bd..f8d82ea 100644 --- a/protocol/http/go.sum +++ b/protocol/http/go.sum @@ -7,6 +7,7 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= @@ -14,6 +15,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -22,6 +25,8 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4 github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -40,7 +45,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -51,11 +59,18 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -69,8 +84,11 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/protocol/http/main.go b/protocol/http/main.go index 5c61254..5bdc1f0 100644 --- a/protocol/http/main.go +++ b/protocol/http/main.go @@ -1,20 +1,239 @@ package main import ( + "encoding/base64" + "encoding/json" + "errors" + "flag" + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/yaml.v3" "net/http" + "os" + "strings" + "syscall" + "time" "github.com/gin-gonic/gin" ) +var globalConfig ServerConfig + func main() { + + var configPath string + flag.StringVar(&configPath, "config", "app-local.yml", "Path to the config file") + flag.Parse() + + yfile, err := os.ReadFile(configPath) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + err = yaml.Unmarshal(yfile, &globalConfig) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + zap.S().Infof("node name = %v , host = %v , port = %v", globalConfig.NodeInfo.Name, globalConfig.NodeInfo.Host, globalConfig.NodeInfo.Port) + InitRabbitCon() + r := gin.Default() + initLog() + r.POST("/handler", HandlerMessage) r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) - err := r.Run() + + err = r.Run(fmt.Sprintf(":%d", globalConfig.NodeInfo.Port)) if err != nil { return } -} \ No newline at end of file +} + +type Param struct { + Data string `json:"data" binding:"required"` +} + +func HandlerMessage(c *gin.Context) { + authHeader := c.Request.Header.Get("Authorization") + // 检查Authorization头部是否符合基本认证格式 + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "请求头 authorization 丢失"}) + return + } + // 解析用户名和密码 + user, pass, ok := parseBasicAuth(authHeader) + zap.S().Infof("解析的账号密码 username: %s, password: %s", user, pass) + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "请求头 authorization 无法正常解析"}) + return + } + deviceId := c.Request.Header.Get("device_id") + username, password := FindDeviceMappingUP(deviceId) + + zap.S().Infof("device_id: %s", deviceId) + zap.S().Infof("有效账号密码 username: %s, password: %s", username, password) + if username != user || password != pass { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "账号密码不匹配"}) + return + } + + var param Param + if err := c.ShouldBindJSON(¶m); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "参数错误"}) + return + } + + mqttMsg := HttpMessage{ + Uid: deviceId, + Message: param.Data, + } + jsonData, err := json.Marshal(mqttMsg) + if err != nil { + zap.S().Errorf("Error marshalling HTTP message to JSON: %v", err) + return + } + PushToQueue("pre_http_handler", jsonData) + + c.JSON(http.StatusOK, gin.H{ + "message": "成功获取数据", + }) +} + +type HttpMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +func FindDeviceMappingUP(deviceId string) (string, string) { + // todo: 从redis中根据deviceId获取用户名和密码 + return "guest", "guest" +} + +func parseBasicAuth(authHeader string) (username, password string, ok bool) { + // 基本认证格式:"Basic " + const prefix = "Basic " + if len(authHeader) < len(prefix) || authHeader[:len(prefix)] != prefix { + return + } + + // 解码base64 + enc := authHeader[len(prefix):] + decoded, err := base64.StdEncoding.DecodeString(enc) + if err != nil { + return + } + + // 解析用户名和密码 + cs := string(decoded) + if !strings.Contains(cs, ":") { + return + } + split := strings.Split(cs, ":") + return split[0], split[1], true +} + +var myTimeEncoder = zapcore.TimeEncoder(func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + // 按照 "2006-01-02 15:04:05" 的格式编码时间 + enc.AppendString(t.Format("2006-01-02 15:04:05")) +}) + +func initLog() { + encoderConfig := zapcore.EncoderConfig{ + // 使用自定义的时间编码器 + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stack", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码日志级别 + EncodeTime: myTimeEncoder, // 使用自定义的时间编码器 + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, // 短路径编码调用者 + } + + core := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), // 使用 Console 编码器 + zapcore.AddSync(os.Stdout), // 输出到标准输出 + zap.NewAtomicLevelAt(zap.InfoLevel), // 设置日志级别为 Debug + ) + + lg := zap.New(core, zap.AddCaller()) + zap.ReplaceGlobals(lg) // 替换全局 Logger + + // 确保日志被刷新 + defer func(lg *zap.Logger) { + err := lg.Sync() + if err != nil && !errors.Is(err, syscall.ENOTTY) { + zap.S().Errorf("日志同步失败 %+v", err) + } + }(lg) + + // 记录一条日志作为示例 + lg.Debug("这是一个调试级别的日志") +} + +// ServerConfig 定义了服务器配置的结构体,包含了节点信息、Redis配置和消息队列配置。 +type ServerConfig struct { + // NodeInfo 定义了节点的信息,包括主机地址、端口、节点名称、节点类型和最大处理数量。 + NodeInfo NodeInfo `yaml:"node_info" json:"node_info"` + + // RedisConfig 定义了Redis服务器的配置,包括主机地址、端口、数据库索引和密码。 + RedisConfig RedisConfig `yaml:"redis_config" json:"redis_config"` + + // MQConfig 定义了消息队列服务器的配置,包括主机地址、端口、用户名和密码。 + MQConfig MQConfig `yaml:"mq_config" json:"mq_config"` +} + +// NodeInfo 定义了节点的基本信息。 +type NodeInfo struct { + // Host 表示节点的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示节点监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Name 表示节点的名称。 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Type 表示节点的类型。 + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // Size 表示节点可以处理的最大数量。 + Size int64 `json:"size,omitempty" yaml:"size,omitempty"` +} + +// RedisConfig 定义了Redis服务器的配置信息。 +type RedisConfig struct { + // Host 表示Redis服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示Redis服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Db 表示Redis服务器的数据库索引。 + Db int `json:"db,omitempty" yaml:"db,omitempty"` + + // Password 表示Redis服务器的访问密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} + +// MQConfig 定义了消息队列服务器的配置信息。 +type MQConfig struct { + // Host 表示消息队列服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示消息队列服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Username 表示用于访问消息队列服务器的用户名。 + Username string `json:"username,omitempty" yaml:"username,omitempty"` + + // Password 表示用于访问消息队列服务器的密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} diff --git a/protocol/http/rabbit_mq.go b/protocol/http/rabbit_mq.go new file mode 100644 index 0000000..f97222d --- /dev/null +++ b/protocol/http/rabbit_mq.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "go.uber.org/zap" +) + +var GRabbitMq *amqp.Connection + +func CreateRabbitQueue(queueName string) { + + ch, err := GRabbitMq.Channel() + if err != nil { + zap.S().Fatalf("Failed to open a channel %v", err) + } + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _, err = ch.QueueDeclare(queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + zap.S().Fatalf("创建queue异常 %s", queueName) + } +} +func InitRabbitCon() { + conn, err := amqp.Dial(genUrl()) + if err != nil { + zap.S().Fatalf("Failed to connect to RabbitMQ %v", err) + } + + GRabbitMq = conn + + CreateRabbitQueue("pre_tcp_handler") + +} +func genUrl() string { + connStr := fmt.Sprintf("amqp://%s:%s@%s:%d/", globalConfig.MQConfig.Username, globalConfig.MQConfig.Password, globalConfig.MQConfig.Host, globalConfig.MQConfig.Port) + return connStr +} + +// PushToQueue 将消息推送到RabbitMQ队列中 +// +// 参数: +// queue_name: string类型,目标队列的名称 +// body: []byte类型,待发送的消息体 +// +// 返回值: +// 无返回值 +func PushToQueue(queueName string, body []byte) { + + ch, _ := GRabbitMq.Channel() + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _ = ch.PublishWithContext(context.Background(), "", queueName, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: body, + }) + zap.S().Infof(" [x] 发送到 %s 消息体 %s", queueName, body) + +} diff --git a/protocol/readme.md b/protocol/readme.md index 512d23a..860f623 100644 --- a/protocol/readme.md +++ b/protocol/readme.md @@ -1,14 +1,5 @@ # 扩展协议支持 -1. coap -2. modbus -3. http - -coap 无法直接支持用户认证 , 需要通过一个额外的接口进行判断 - - -http 可以支持用户认证 , 请求头中直接携带标识即可 -1. username + password ## TCP @@ -55,3 +46,20 @@ datadata 数据已处理. ``` + +## HTTP + +1. 账号密码认证模式 basicc auth + +![image-20240725103427236](readme/image-20240725103427236.png) + +2. 请求头中需要标注设备id。键名为`device_id` + +3. 请求体结构为 + +``` +{ + "data":"字符串" +} +``` + diff --git a/protocol/readme/image-20240725103427236.png b/protocol/readme/image-20240725103427236.png new file mode 100644 index 0000000000000000000000000000000000000000..b0f225457c024d04df8da756e93f88a0806d2211 GIT binary patch literal 181129 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

1StnBWQhl6RUM`%WXNK4+2Y~ab+he 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üt=caQmgEPdjVh8io*> zlPSv((gr2{u&#yLW=c*%cQ|<(|G_MrpHIG1x!0IOf>rJ7|Fv#z&U~-32=mX&9H&s? z&YfqtikF~IFUPe3-tmqD7hqL7Pzd4lzG3&Zhwgk_p)WBB2_(ig{Kj5yK*!XAF#geR z;dCZKj)4>nW!6xh{G6v^{N(usjnBqUVFNc>_YhFO#!CU4G)h#bE_A+avs?^vp#D<4uTkbY5tfek#@0pT~kP z{%45&l}IaNVL(!1U9si^pV@RF&-nt@CHB%WQXb`Vp@VQoILpx z>cd_n6DNgOQm76go^j^6&EAX=D!mGju+%e8KIq_JB^!A;d+N$Uc2PEL6t1$DzE4XC zC5jYe9kw{W!%lrV%NZ%xUXc~5oO;^0 zAc$!(^=kdW@otxo_PMZZ((Ejq%90@kpQ*fCl$dG<=geJRZGRAF8C0aW(fV>8^2?!X zeGZ^Js91k+#4F0^D`A0lD8};4E$V!r+8=M8Q~<3bG@bG&9`~Byj$G8&(ke=0PVSF69kt^K<25{ptYRi{*(u%n5q%{k+tOAt<|d^MnV>e|e)nSmDt zSP`5FeR-uICkZai8p9@G3D6UVDO7J~+OTayG$C=rq$Z7h0}uu0jZBH#Mk>lTrrVx( zMCmAKT$op5ZsR+Ty8Tu#T70O$FqpCtzzr)%YMk0MUrWS@yC}X&a#mIiLl?0av~79D zY@;C#>vJOM{sp$xAN9E0Wf$Y+-oTINV(1v%@p}O&ZsFY(g|J-pK#4TS5rWziq&A!# z)Bg0<0`KX=Y&x#%X$e!U)v;Hc-lfkC7H1_a>P{Ek<>Pel-xELlt^@?Za&7vB(W*)W zO5%!5<$d$KAA-$@IxtH`V|G91XbB2NJhIt~00&IWluqckctD>z2g(qe>T}UDDkV>T z9x(__6jt^~2U@y*1dT;&_9xU89DH_p8OIrIp!p$_lS(6I#@*)9g0*i5Q|zH(*VtmZ zAi-_{OnEu;X7a@2O)(!|I)!P+kjJQ-(i;B>C5Cgr)^NR!^NGBI*N?@4o?SC>ee=+eGA(J(@Fg+rc;g-8Tw-1%65}`=M^4lG}hzJsUzO2PN?mizVY!k02ZVbKZ z3pa`!wVxNPTBIL&Q@xN|`B~)oCKl-xwAReC6{U%|fBrJ+Gdr-JEC z-F{kazlfLyZq!TPo7yZK?{54#M}kWR;viyKjR;!&>RD4ABJ1j12DEv9e*yp8!SJR8 zF`v7Ud?7&I+W*dEC6;l^#ue`0zrP$2K=gn4IfxUnGbkLs*)u5^e@;MtB_(t#SQwYr z&is$W5vQ~kII-4%I0y&*M5n=i$`^Rf< zlGO*eP5{W8Dv_ChYfeO&*#siG3n=qVfdGAZgnJR_(ZH;vM|BmbBL%HBxb^hHu;ui5 zZveEBTnGqK+qX9x>fqr>=n^*a5}_NQTg}vn)T*TFXqaCgLQ&>6kA60rLdZ_v^%f|x z1Td|Cbe90ha1f+zM5CSPf4p0)4`lsCrV&od!vn1^E+&{X0f$`+N($|9SxWimY)?&6 z42z{S#45W($U z(%u>$pW_UV-b27iS%dn%XPN<)q?0lcEvHV-dp`gni(f^cLU8SMp6%WJk_;~WS-_7@ zdhq#%fWuU?oh?L>vNns)P8Qk6hbulh<`sY@^PvcEkw^R97yWx(Gk4V-wZmSqKOau_ zPTl#dS?n9K5dh7WTkq9>_5L3Ttx%x^n7tulPkR^b8t*E0@-&YJFxvF~uvd}gH@J~$ zbb83Ni=cpy^XIc@C3c^G`&zsETE^&e>>t2Vt+cLmS#kCwJN#5y0x3;vPG1DHYZLrA zE6!cUw0NNXz+3>c00ckZw_2Jb5F2}-#Z3F_88(I$fcB>Bjx`RQ^+4fAsF_3rXW#%I z0}&mQ7IYI9RQ(D-0#d=t(O?c`POhkqY-fuY7M*e#rvIbyspw4KFxu3$CepaTbSIq2 z9Sxx)F~}bNbkNp*!an%Xqm&tpjxQYT

#5WqUl3JO^O*`m{f9#UrRXx;om@>7Erj z*pyA2q6FWJ%37F+&=~zd1QC{(mqN}g^fge={^QvRud`H$#vPOU?n^FrS`33`03$vq>QnQrXI9Y71~a+7yNO*`-R~qy3NG( zR(ZH&LE6JU_mo7Tjdm_JiV!zblqN*!WVCpH6EL*AW4k` zJ3=Ko*YR3E*C9u%1U;^cO^VH-f2^1!=$`oc3vyt-=&^>Fz_lPZd+*$1y}es`#o2gd zmyI!jnOk?UY43Jd0^@6No#JYnMnt|)G_&^FXy}sZv|Jy!#4(ZSW=pDG@jWx4-iy<< z$Y$h<+9Sn{ZL?)cOvh#vHc0`zcwUUV>%34=6B?>?=G^IS?5bFsMYS{!uqVABV&0Pc z(+NdaT?;CTB``z4T6xtT34-B3J=(TFsIx~qS z9fghgxro{5&~$q?whTAhm;AQS1292G{`io0!J=-)BxUN{j5p_q%H)<8wO&)^QpR7aw44<^a`>?Rm$xmfls+%W_7fB+fGRpPv)sdmJv zz}lpL4IH*|e2$NDWssu80I@+4kq2WS9p^{O$4K}go%)n0WWk{T;a!mI#3x!;PFLcQ zO9bKJq4)R{(9)18m|zP>>7`zgm*P5!O{RV)^|}{_?+*~GR33Igf;I%0A7xbI;cM~3 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 literal 0 HcmV?d00001 diff --git a/protocol/tcp/server.go b/protocol/tcp/server.go index 95500a9..82b4f16 100644 --- a/protocol/tcp/server.go +++ b/protocol/tcp/server.go @@ -136,7 +136,7 @@ func handlerData(server *Server, message string, client *Client) { } jsonData, err := json.Marshal(mqttMsg) if err != nil { - zap.S().Errorf("Error marshalling MQTT message to JSON: %v", err) + zap.S().Errorf("Error marshalling TCP message to JSON: %v", err) return } PushToQueue("pre_tcp_handler", jsonData) -- Gitee From 4adf413c956dc4d4b702bc0993eba0bd816cd392 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Thu, 25 Jul 2024 11:15:15 +0800 Subject: [PATCH 35/90] =?UTF-8?q?feat:=20http=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_http_storage.go | 144 ++++++++++++++++++++++++++++++ go-iot-mq/handler_tcp_storage.go | 4 +- go-iot-mq/main.go | 39 +++++--- protocol/http/rabbit_mq.go | 2 +- 4 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 go-iot-mq/handler_http_storage.go diff --git a/go-iot-mq/handler_http_storage.go b/go-iot-mq/handler_http_storage.go new file mode 100644 index 0000000..8303443 --- /dev/null +++ b/go-iot-mq/handler_http_storage.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + "strconv" + "time" +) + +// HttpMessage 用于处理tcp转发后的数据 +type HttpMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +// HandlerHttpDataStorage 函数处理从AMQP通道接收到的HTTP消息数据 +// 参数: +// +// messages <-chan amqp.Delivery:接收AMQP消息的通道 +// +// 返回值: +// +// 无 +func HandlerHttpDataStorage(messages <-chan amqp.Delivery) { + + go func() { + + for d := range messages { + HandlerDataHttpStorageString(d) + err := d.Ack(false) + if err != nil { + zap.S().Errorf("消息确认异常:%+v", err) + + } + } + }() + + zap.S().Infof(" [*] Waiting for messages. To exit press CTRL+C") +} + +func HandlerDataHttpStorageString(d amqp.Delivery) { + var msg HttpMessage + err := json.Unmarshal(d.Body, &msg) + if err != nil { + zap.S().Infof("Failed to unmarshal message: %s", err) + return + } + zap.S().Infof("处理 pre_handler 数据 : %+v", msg) + + script := GetScriptRedisForHttp(msg.Uid) + if script != "" { + data := runScript(msg.Message, script) + for i := 0; i < len(*data); i++ { + row := (*data)[i] + StorageDataRowList(row) + } + zap.S().Debugf("DataRowList: %+v", data) + + jsonData, err := json.Marshal(data) + if err != nil { + zap.S().Errorf("推送报警原始数据异常 %s", err) + return + } + zap.S().Infof("推送报警原始数据: %s", jsonData) + writeAPI.Flush() + HandlerHttpLastTime(*data) + PushToQueue("waring_handler", jsonData) + PushToQueue("waring_delay_handler", jsonData) + PushToQueue("transmit_handler", jsonData) + } else { + zap.S().Infof("执行脚本为空") + } + +} + +// HandlerHttpLastTime 和上一次推送事件进行对比,判断是否超过阈值,如果超过则发送额外的消息通知 +func HandlerHttpLastTime(data []DataRowList) { + if len(data) == 0 { + return + } + + var deviceUid = data[0].DeviceUid + key := "last_push_time:" + deviceUid + // 1. 从redis中获取这个设备上次推送的时间 + lastTime, err := globalRedisClient.Get(context.Background(), key).Result() + if err != nil && !errors.Is(err, redis.Nil) { + zap.S().Errorf("获取设备上次推送时间异常:%+v", err) + return + } + now := time.Now().Unix() + + // 如果没有这个时间则设置时间(当前时间) + if errors.Is(err, redis.Nil) { + err := globalRedisClient.Set(context.Background(), key, now, 0).Err() + if err != nil { + zap.S().Errorf("设置设备上次推送时间异常:%+v", err) + return + } + lastTime = fmt.Sprintf("%d", now) + } + + if lastTime != fmt.Sprintf("%d", now) { + + val := globalRedisClient.LRange(context.Background(), "http_bind_device_info:"+deviceUid, 0, -1).Val() + + for _, s := range val { + handlerHttpOne(s) + } + + } + +} + +func handlerHttpOne(deviceUid string) bool { + val := globalRedisClient.Get(context.Background(), "http_bind_device_info:"+deviceUid).Val() + if val == "" { + return true + } + parseUint, _ := strconv.ParseUint(val, 10, 64) + withRedis := FindByIdWithRedis(parseUint) + if withRedis == nil { + return true + } + globalRedisClient.Expire(context.Background(), "Device_Off_Message:"+deviceUid, time.Duration(withRedis.PushInterval)*time.Second) + return false +} + +// GetScriptRedisForHttp 根据 http 的设备ID从Redis中获取对应的脚本 +// 参数: +// +// tcp id string - tcp id +// +// 返回值: +// +// string - 对应的脚本 +func GetScriptRedisForHttp(tcpId string) string { + val := globalRedisClient.HGet(context.Background(), "struct:Http", tcpId).Val() + return val +} diff --git a/go-iot-mq/handler_tcp_storage.go b/go-iot-mq/handler_tcp_storage.go index e162f60..2e5974c 100644 --- a/go-iot-mq/handler_tcp_storage.go +++ b/go-iot-mq/handler_tcp_storage.go @@ -12,13 +12,13 @@ import ( "time" ) -// 用于处理tcp转发后的数据 +// TcpMessage 用于处理tcp转发后的数据 type TcpMessage struct { Uid string `json:"uid"` Message string `json:"message"` } -// HandlerDataStorage 函数处理从AMQP通道接收到的MQTT消息数据 +// HandlerTcpDataStorage 函数处理从AMQP通道接收到的TCP消息数据 // 参数: // // messages <-chan amqp.Delivery:接收AMQP消息的通道 diff --git a/go-iot-mq/main.go b/go-iot-mq/main.go index b12bd2c..cc9d0a9 100644 --- a/go-iot-mq/main.go +++ b/go-iot-mq/main.go @@ -71,40 +71,59 @@ func main() { cus.Handle(deliveries, HandlerDataStorage, 1, "pre_handler", "") } if globalConfig.NodeInfo.Type == "waring_handler" { - waring_handler, err := cus.AnnounceQueue("waring_handler", "") + waringHandler, err := cus.AnnounceQueue("waring_handler", "") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %s", err) } - cus.Handle(waring_handler, HandlerWaring, 1, "waring_handler", "") + cus.Handle(waringHandler, HandlerWaring, 1, "waring_handler", "") } if globalConfig.NodeInfo.Type == "calc_queue" { - calc_queue, err := cus.AnnounceQueue("calc_queue", "") + calcQueue, err := cus.AnnounceQueue("calc_queue", "") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %s", err) } - cus.Handle(calc_queue, HandlerCalc, 1, "calc_queue", "") + cus.Handle(calcQueue, HandlerCalc, 1, "calc_queue", "") } if globalConfig.NodeInfo.Type == "waring_delay_handler" { - waring_delay_handler, err := cus.AnnounceQueue("waring_delay_handler", "") + waringDelayHandler, err := cus.AnnounceQueue("waring_delay_handler", "") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %s", err) } - cus.Handle(waring_delay_handler, HandlerWaringDelay, 1, "waring_delay_handler", "") + cus.Handle(waringDelayHandler, HandlerWaringDelay, 1, "waring_delay_handler", "") } if globalConfig.NodeInfo.Type == "transmit_handler" { - transmit_handler, err := cus.AnnounceQueue("transmit_handler", "") + transmitHandler, err := cus.AnnounceQueue("transmit_handler", "") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %s", err) } - cus.Handle(transmit_handler, HandlerTransmit, 1, "transmit_handler", "") + cus.Handle(transmitHandler, HandlerTransmit, 1, "transmit_handler", "") } if globalConfig.NodeInfo.Type == "waring_notice" { - waring_notice, err := cus.AnnounceQueue("waring_notice", "") + waringNotice, err := cus.AnnounceQueue("waring_notice", "") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %s", err) } - cus.Handle(waring_notice, HandlerNotice, 1, "waring_notice", "") + cus.Handle(waringNotice, HandlerNotice, 1, "waring_notice", "") + } + + + + // 协议层处理 + if globalConfig.NodeInfo.Type == "pre_tcp_handler" { + preTcpHandler, err := cus.AnnounceQueue("pre_tcp_handler", "") + if err != nil { + log.Fatalf("Failed to connect to RabbitMQ: %s", err) + } + cus.Handle(preTcpHandler, HandlerTcpDataStorage, 1, "pre_tcp_handler", "") + } + + if globalConfig.NodeInfo.Type == "pre_http_handler" { + preHttpHandler, err := cus.AnnounceQueue("pre_http_handler", "") + if err != nil { + log.Fatalf("Failed to connect to RabbitMQ: %s", err) + } + cus.Handle(preHttpHandler, HandlerHttpDataStorage, 1, "pre_http_handler", "") } } diff --git a/protocol/http/rabbit_mq.go b/protocol/http/rabbit_mq.go index f97222d..d17a983 100644 --- a/protocol/http/rabbit_mq.go +++ b/protocol/http/rabbit_mq.go @@ -42,7 +42,7 @@ func InitRabbitCon() { GRabbitMq = conn - CreateRabbitQueue("pre_tcp_handler") + CreateRabbitQueue("pre_http_handler") } func genUrl() string { -- Gitee From 7ae70dcb8b475952fcd5135810a051431f1cd490 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Thu, 25 Jul 2024 12:27:43 +0800 Subject: [PATCH 36/90] =?UTF-8?q?fix:=20http=20=E8=B4=A6=E5=8F=B7=E4=BD=93?= =?UTF-8?q?=E7=B3=BB=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/go.mod | 29 +++++- go-iot-mq/go.sum | 99 +++++++++++++++++++++ go-iot-mq/z_test.go | 10 ++- iot-go-project/models/models.go | 6 +- iot-go-project/router/device_info_router.go | 94 +++++++------------ protocol/http/go.mod | 5 +- protocol/http/go.sum | 10 +++ protocol/http/main.go | 16 +++- protocol/http/redis.go | 46 ++++++++++ 9 files changed, 239 insertions(+), 76 deletions(-) create mode 100644 protocol/http/redis.go diff --git a/go-iot-mq/go.mod b/go-iot-mq/go.mod index b33938c..3773c55 100644 --- a/go-iot-mq/go.mod +++ b/go-iot-mq/go.mod @@ -11,6 +11,8 @@ require iot-notice v0.0.0 replace iot-notice => ../notice require ( + github.com/CatchZeng/feishu v1.3.2 + github.com/blinkbean/dingtalk v1.1.3 github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 github.com/influxdata/influxdb-client-go/v2 v2.13.0 github.com/prometheus/client_golang v1.19.1 @@ -20,34 +22,54 @@ require ( go.mongodb.org/mongo-driver v1.16.0 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.25.11 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/ClickHouse/ch-go v0.61.5 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.26.0 // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-resty/resty/v2 v2.7.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/gocql/gocql v1.6.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.4.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect + github.com/paulmach/orb v0.11.1 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/shirou/gopsutil v2.19.11+incompatible // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect @@ -55,4 +77,5 @@ require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect ) diff --git a/go-iot-mq/go.sum b/go-iot-mq/go.sum index 01dff0c..54f0485 100644 --- a/go-iot-mq/go.sum +++ b/go-iot-mq/go.sum @@ -1,10 +1,24 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/CatchZeng/feishu v1.3.2 h1:3A51zYAvxGwPHSy5MGL0thM7o7v/+ufgGfVEbKkd1Ps= +github.com/CatchZeng/feishu v1.3.2/go.mod h1:osX8HjZ4feBHq6F0ggxor4/VSNM3CpyqlWKZZ4IVsdw= +github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= +github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= +github.com/ClickHouse/clickhouse-go/v2 v2.26.0 h1:j4/y6NYaCcFkJwN/TU700ebW+nmsIy34RmUAAcZKy9w= +github.com/ClickHouse/clickhouse-go/v2 v2.26.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4= +github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -24,29 +38,59 @@ github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnm github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 h1:4Ew88p5s9dwIk5/woUyqI9BD89NgZoUNH4/rM/h2UDg= github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= +github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -58,9 +102,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= @@ -77,70 +129,105 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.5.4 h1:vOFYDKKVgrI5u++QvnMT7DksSMYg7Aw/Np4vLJLKLwY= github.com/redis/go-redis/v9 v9.5.4/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -148,6 +235,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -158,25 +246,36 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/go-iot-mq/z_test.go b/go-iot-mq/z_test.go index 86db4c4..a7eab49 100644 --- a/go-iot-mq/z_test.go +++ b/go-iot-mq/z_test.go @@ -2,6 +2,8 @@ package main import ( "context" + "encoding/json" + "strconv" "testing" ) @@ -9,12 +11,16 @@ func TestA(t *testing.T) { var config = RedisConfig{ Host: "127.0.0.1", Port: 6379, - Db: 0, + Db: 1, Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81", } InitGlobalRedisClient(config) + globalRedisClient.Set(context.Background(), "a", 1, 0) + jsonData, _ := json.Marshal(config) + + globalRedisClient.HSet(context.Background(), "auth:http",strconv.Itoa(int(1)), jsonData) + - globalRedisClient.ZRemRangeByScore(context.Background(), "aaa", "-inf", "0") } func TestMqCustomer(t *testing.T) { diff --git a/iot-go-project/models/models.go b/iot-go-project/models/models.go index 25f0040..614bbd2 100644 --- a/iot-go-project/models/models.go +++ b/iot-go-project/models/models.go @@ -258,13 +258,9 @@ type TcpHandler struct { Script string `json:"script" structs:"script"` // 处理器脚本 } -type DeviceBindHTTPHandler struct { - gorm.Model `structs:"-"` - DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID - HttpHandlerId uint `json:"http_handler_id" structs:"http_handler_id"` // HTTP处理器的ID -} type HttpHandler struct { + DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID Name string `json:"name" structs:"name"` // 处理器名 Username string `json:"username" structs:"username"` // 用户名 Password string `json:"password" structs:"password"` // 密码 diff --git a/iot-go-project/router/device_info_router.go b/iot-go-project/router/device_info_router.go index adcedeb..5a7a600 100644 --- a/iot-go-project/router/device_info_router.go +++ b/iot-go-project/router/device_info_router.go @@ -2,6 +2,7 @@ package router import ( "context" + "encoding/json" "github.com/fatih/structs" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -238,7 +239,7 @@ func (api *DeviceInfoApi) QueryBindMqtt(c *gin.Context) { func (api *DeviceInfoApi) QueryBindHttp(c *gin.Context) { param := c.Param("device_info_id") - var res []models.DeviceBindHTTPHandler + var res []models.DeviceBindTcpHandler // 使用 Where 和 Find 方法查询记录 result := glob.GDb.Where("`device_info_id` = ?", param).Find(&res) @@ -249,6 +250,7 @@ func (api *DeviceInfoApi) QueryBindHttp(c *gin.Context) { } servlet.Resp(c, res) } + // QueryBindTcp // @Tags DeviceInfos // @Summary 查询绑定tcp客户端 @@ -449,79 +451,45 @@ func (api *DeviceInfoApi) BindTcp(c *gin.Context) { // @Param DeviceGroup body servlet.DeviceBindHTTPParam true "绑定参数" // @Router /DeviceInfo/BindHTTP [post] func (api *DeviceInfoApi) BindHTTP(c *gin.Context) { - var param servlet.DeviceBindHTTPParam + var param models.HttpHandler if err := c.ShouldBindJSON(¶m); err != nil { - servlet.Error(c, err.Error()) return } - // 开启事务 - tx := glob.GDb.Begin() - if tx.Error != nil { - servlet.Error(c, "Failed to begin transaction") - return - } - var toDel []models.DeviceBindHTTPHandler - - tx.Where("`device_info_id` = ?", param.DeviceId).Find(toDel) - - result := tx.Where("`device_info_id` = ?", param.DeviceId).Delete(&models.DeviceBindHTTPHandler{}) - - if result.Error != nil { - // 如果出现错误,回滚事务 - tx.Rollback() - servlet.Error(c, "Error occurred during deletion") - return - } - - var deviceBindHTTPHandlers []models.DeviceBindHTTPHandler - for _, httpId := range param.HttpHandlerId { - deviceBindHTTPHandlers = append(deviceBindHTTPHandlers, models.DeviceBindHTTPHandler{ - DeviceInfoId: uint(param.DeviceId), - HttpHandlerId: uint(httpId), - }) - } - - result = tx.Model(&models.DeviceBindHTTPHandler{}).CreateInBatches(deviceBindHTTPHandlers, len(deviceBindHTTPHandlers)) - if result.Error != nil { - tx.Rollback() - zap.S().Infoln("Error occurred during creation:", result.Error) - servlet.Error(c, "Error occurred during creation") - return - } - if err := tx.Commit().Error; err != nil { - servlet.Error(c, "Failed to commit transaction") - return - } - - // redis 中建立 tcp 与 device_info_id 的映射 - - var DeviceInfo models.DeviceInfo - - first := tx.First(&DeviceInfo, param.DeviceId) - if first.Error != nil { - servlet.Error(c, "DeviceInfo not found") - return - } + if param.ID != 0 { + var old models.HttpHandler - for _, client := range toDel { - glob.GRedis.Del(context.Background(), "tcp_bind_product:"+strconv.Itoa(int(client.HttpHandlerId))) - } + result := glob.GDb.First(&old, param.ID) + if result.Error != nil { - for _, item := range param.HttpHandlerId { - glob.GRedis.LPush(context.Background(), "tcp_bind_product:"+strconv.Itoa(item), DeviceInfo.ProductId) - } + servlet.Error(c, "HttpHandler not found") + return + } + var newV models.HttpHandler + newV = old + newV.DeviceInfoId = param.DeviceInfoId + newV.Name = param.Name + newV.Username = param.Username + newV.Password = param.Password + newV.Script = param.Script + // 更新记录 + result = glob.GDb.Model(&newV).Updates(newV) + setHttpHandlerRedis(newV) + } else { + // 新增 + glob.GDb.Model(models.HttpHandler{}).Create(¶m) + setHttpHandlerRedis(param) - for _, client := range toDel { - glob.GRedis.Del(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(int(client.HttpHandlerId))) } - for _, item := range param.HttpHandlerId { - glob.GRedis.LPush(context.Background(), "tcp_bind_device_info:"+strconv.Itoa(item), DeviceInfo.ID) - - } + glob.GRedis.LPush(context.Background(), "http_bind_device_info:"+strconv.Itoa(int(param.ID)), param.DeviceInfoId) servlet.Resp(c, "绑定成功") } + +func setHttpHandlerRedis(config models.HttpHandler){ + jsonData, _ := json.Marshal(config) + glob.GRedis.HSet(context.Background(), "auth:http",strconv.Itoa(int(config.DeviceInfoId)), jsonData) +} \ No newline at end of file diff --git a/protocol/http/go.mod b/protocol/http/go.mod index 229de3e..b10e4c5 100644 --- a/protocol/http/go.mod +++ b/protocol/http/go.mod @@ -5,14 +5,18 @@ go 1.22.4 require ( github.com/gin-gonic/gin v1.10.0 github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.6.0 go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -35,5 +39,4 @@ require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/protocol/http/go.sum b/protocol/http/go.sum index f8d82ea..bf19b12 100644 --- a/protocol/http/go.sum +++ b/protocol/http/go.sum @@ -1,7 +1,13 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -9,6 +15,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -49,6 +57,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= +github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/protocol/http/main.go b/protocol/http/main.go index 5bdc1f0..255fe38 100644 --- a/protocol/http/main.go +++ b/protocol/http/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/base64" "encoding/json" "errors" @@ -38,7 +39,7 @@ func main() { zap.S().Infof("node name = %v , host = %v , port = %v", globalConfig.NodeInfo.Name, globalConfig.NodeInfo.Host, globalConfig.NodeInfo.Port) InitRabbitCon() - + InitGlobalRedisClient(globalConfig.RedisConfig) r := gin.Default() initLog() r.POST("/handler", HandlerMessage) @@ -109,9 +110,20 @@ type HttpMessage struct { Message string `json:"message"` } +type Auth struct { + Username string `json:"username"` + Password string `json:"password"` +} + func FindDeviceMappingUP(deviceId string) (string, string) { // todo: 从redis中根据deviceId获取用户名和密码 - return "guest", "guest" + val := globalRedisClient.HGet(context.Background(), "auth:http", deviceId).Val() + var auth Auth + err := json.Unmarshal([]byte(val), &auth) + if err != nil { + return "", "" + } + return auth.Username, auth.Password } func parseBasicAuth(authHeader string) (username, password string, ok bool) { diff --git a/protocol/http/redis.go b/protocol/http/redis.go new file mode 100644 index 0000000..72c4ac8 --- /dev/null +++ b/protocol/http/redis.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +var globalRedisClient *redis.Client + +// InitGlobalRedisClient 初始化全局Redis客户端 +// +// 参数: +// config RedisConfig - Redis配置信息 +// +// 返回值: +// 无 +func InitGlobalRedisClient(config RedisConfig) { + + add := fmt.Sprintf("%s:%d", config.Host, config.Port) + globalRedisClient = redis.NewClient(&redis.Options{ + Addr: add, + Password: config.Password, // 如果没有设置密码,就留空字符串 + DB: config.Db, // 使用默认数据库 + }) + + // 检查连接是否成功 + if err := globalRedisClient.Ping(context.Background()).Err(); err != nil { + zap.S().Fatalf("Could not connect to Redis: %v", err) + } + +} + +// GetScriptRedis 根据MQTT客户端ID从Redis中获取对应的脚本 +// 参数: +// +// mqttClientId string - MQTT客户端ID +// +// 返回值: +// +// string - 对应的脚本 +func GetScriptRedis(mqttClientId string) string { + val := globalRedisClient.HGet(context.Background(), "mqtt_script", mqttClientId).Val() + return val +} -- Gitee From e514e798a68602e6f1441882d9e60a6f77f18f08 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Thu, 25 Jul 2024 17:04:21 +0800 Subject: [PATCH 37/90] =?UTF-8?q?fix:=20coap=20=E5=8D=8F=E8=AE=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go-iot-mq/handler_coap_storage.go | 144 +++++++++++++++ go-iot-mq/z_test.go | 17 +- iot-go-project/biz/coap_biz.go | 47 +++++ iot-go-project/initialize/init.go | 9 +- iot-go-project/models/models.go | 16 +- iot-go-project/router/coap_handler_router.go | 181 +++++++++++++++++++ iot-go-project/router/device_info_router.go | 81 ++++++++- protocol/coap/app-local.yml | 16 ++ protocol/coap/go.mod | 15 +- protocol/coap/go.sum | 30 +++ protocol/coap/main.go | 138 ++++++++++++++ protocol/coap/rabbit_mq.go | 81 +++++++++ protocol/coap/redis.go | 26 +++ protocol/coap/redis_lock.go | 84 +++++++++ protocol/coap/sample/client/main.go | 54 +++++- protocol/coap/server.go | 160 ++++++++++++++++ protocol/modbus/app-local.yml | 16 ++ protocol/modbus/go.mod | 13 +- protocol/modbus/go.sum | 28 +++ protocol/modbus/main.go | 93 ++++++++++ protocol/modbus/rabbit_mq.go | 81 +++++++++ protocol/modbus/redis.go | 26 +++ protocol/modbus/redis_lock.go | 84 +++++++++ protocol/tcp/server.go | 66 ++++--- 24 files changed, 1451 insertions(+), 55 deletions(-) create mode 100644 go-iot-mq/handler_coap_storage.go create mode 100644 iot-go-project/biz/coap_biz.go create mode 100644 iot-go-project/router/coap_handler_router.go create mode 100644 protocol/coap/app-local.yml create mode 100644 protocol/coap/main.go create mode 100644 protocol/coap/rabbit_mq.go create mode 100644 protocol/coap/redis.go create mode 100644 protocol/coap/redis_lock.go create mode 100644 protocol/coap/server.go create mode 100644 protocol/modbus/app-local.yml create mode 100644 protocol/modbus/main.go create mode 100644 protocol/modbus/rabbit_mq.go create mode 100644 protocol/modbus/redis.go create mode 100644 protocol/modbus/redis_lock.go diff --git a/go-iot-mq/handler_coap_storage.go b/go-iot-mq/handler_coap_storage.go new file mode 100644 index 0000000..e2d1253 --- /dev/null +++ b/go-iot-mq/handler_coap_storage.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + "strconv" + "time" +) + +// CoapMessage 用于处理tcp转发后的数据 +type CoapMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +// HandlerCoapDataStorage 函数处理从AMQP通道接收到的coap消息数据 +// 参数: +// +// messages <-chan amqp.Delivery:接收AMQP消息的通道 +// +// 返回值: +// +// 无 +func HandlerCoapDataStorage(messages <-chan amqp.Delivery) { + + go func() { + + for d := range messages { + HandlerDataCoapStorageString(d) + err := d.Ack(false) + if err != nil { + zap.S().Errorf("消息确认异常:%+v", err) + + } + } + }() + + zap.S().Infof(" [*] Waiting for messages. To exit press CTRL+C") +} + +func HandlerDataCoapStorageString(d amqp.Delivery) { + var msg CoapMessage + err := json.Unmarshal(d.Body, &msg) + if err != nil { + zap.S().Infof("Failed to unmarshal message: %s", err) + return + } + zap.S().Infof("处理 pre_handler 数据 : %+v", msg) + + script := GetScriptRedisForCoap(msg.Uid) + if script != "" { + data := runScript(msg.Message, script) + for i := 0; i < len(*data); i++ { + row := (*data)[i] + StorageDataRowList(row) + } + zap.S().Debugf("DataRowList: %+v", data) + + jsonData, err := json.Marshal(data) + if err != nil { + zap.S().Errorf("推送报警原始数据异常 %s", err) + return + } + zap.S().Infof("推送报警原始数据: %s", jsonData) + writeAPI.Flush() + HandlerCoapLastTime(*data) + PushToQueue("waring_handler", jsonData) + PushToQueue("waring_delay_handler", jsonData) + PushToQueue("transmit_handler", jsonData) + } else { + zap.S().Infof("执行脚本为空") + } + +} + +// HandlerCoapLastTime 和上一次推送事件进行对比,判断是否超过阈值,如果超过则发送额外的消息通知 +func HandlerCoapLastTime(data []DataRowList) { + if len(data) == 0 { + return + } + + var deviceUid = data[0].DeviceUid + key := "last_push_time:" + deviceUid + // 1. 从redis中获取这个设备上次推送的时间 + lastTime, err := globalRedisClient.Get(context.Background(), key).Result() + if err != nil && !errors.Is(err, redis.Nil) { + zap.S().Errorf("获取设备上次推送时间异常:%+v", err) + return + } + now := time.Now().Unix() + + // 如果没有这个时间则设置时间(当前时间) + if errors.Is(err, redis.Nil) { + err := globalRedisClient.Set(context.Background(), key, now, 0).Err() + if err != nil { + zap.S().Errorf("设置设备上次推送时间异常:%+v", err) + return + } + lastTime = fmt.Sprintf("%d", now) + } + + if lastTime != fmt.Sprintf("%d", now) { + + val := globalRedisClient.LRange(context.Background(), "coap_bind_device_info:"+deviceUid, 0, -1).Val() + + for _, s := range val { + handlerCoapOne(s) + } + + } + +} + +func handlerCoapOne(deviceUid string) bool { + val := globalRedisClient.Get(context.Background(), "coap_bind_device_info:"+deviceUid).Val() + if val == "" { + return true + } + parseUint, _ := strconv.ParseUint(val, 10, 64) + withRedis := FindByIdWithRedis(parseUint) + if withRedis == nil { + return true + } + globalRedisClient.Expire(context.Background(), "Device_Off_Message:"+deviceUid, time.Duration(withRedis.PushInterval)*time.Second) + return false +} + +// GetScriptRedisForCoap 根据 http 的设备ID从Redis中获取对应的脚本 +// 参数: +// +// tcp id string - tcp id +// +// 返回值: +// +// string - 对应的脚本 +func GetScriptRedisForCoap(tcpId string) string { + val := globalRedisClient.HGet(context.Background(), "struct:Coap", tcpId).Val() + return val +} diff --git a/go-iot-mq/z_test.go b/go-iot-mq/z_test.go index a7eab49..69de7d5 100644 --- a/go-iot-mq/z_test.go +++ b/go-iot-mq/z_test.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "strconv" "testing" ) @@ -11,14 +10,26 @@ func TestA(t *testing.T) { var config = RedisConfig{ Host: "127.0.0.1", Port: 6379, - Db: 1, + Db: 10, Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81", } InitGlobalRedisClient(config) globalRedisClient.Set(context.Background(), "a", 1, 0) jsonData, _ := json.Marshal(config) - globalRedisClient.HSet(context.Background(), "auth:http",strconv.Itoa(int(1)), jsonData) + type Auth struct { + Username string `json:"username"` + Password string `json:"password"` + DeviceId string `json:"device_id"` + } + auth := Auth{ + Username: "admin", + Password: "admin", + DeviceId: "1234567890", + } + jsonData, _ = json.Marshal(auth) + + globalRedisClient.HSet(context.Background(), "auth:coap","1234567890", jsonData) } diff --git a/iot-go-project/biz/coap_biz.go b/iot-go-project/biz/coap_biz.go new file mode 100644 index 0000000..3c4c0ec --- /dev/null +++ b/iot-go-project/biz/coap_biz.go @@ -0,0 +1,47 @@ +package biz + +import ( + "context" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type CoapHandlerBiz struct{} + +func (biz *CoapHandlerBiz) ById(id uint) (*models.CoapHandler, error) { + var CoapHandler models.CoapHandler + + result := glob.GDb.First(&CoapHandler, id) + if result.Error != nil { + return nil, result.Error + } + return &CoapHandler, nil +} + +func (biz *CoapHandlerBiz) PageData(name string, page, size int) (*servlet.PaginationQ, error) { + var pagination servlet.PaginationQ + var CoapHandlerList []models.CoapHandler + + db := glob.GDb + if name != "" { + db = db.Where("name LIKE ?", "%"+name+"%") + } + + db.Model(&models.CoapHandler{}).Count(&pagination.Total) // 计算总记录数 + offset := (page - 1) * size + db.Offset(offset).Limit(size).Find(&CoapHandlerList) + pagination.Data = CoapHandlerList + pagination.Page = page + pagination.Size = size + + return &pagination, nil +} + +func (biz *CoapHandlerBiz) SetRedis(data models.CoapHandler) { + glob.GRedis.HSet(context.Background(), "struct:Coap", strconv.Itoa(int(data.ID)), data.Script) +} +func (biz *CoapHandlerBiz) RemoveRedis(data models.CoapHandler) { + glob.GRedis.HDel(context.Background(), "struct:Coap", strconv.Itoa(int(data.ID))) +} diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index ff54545..83486ec 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -358,13 +358,6 @@ func initTable() { zap.S().Errorf("数据库表创建失败 %+v", err) } } - if !glob.GDb.Migrator().HasTable(&models.DeviceBindHTTPHandler{}) { - - err := glob.GDb.AutoMigrate(&models.DeviceBindHTTPHandler{}) - if err != nil { - zap.S().Errorf("数据库表创建失败 %+v", err) - } - } } func initDb() { @@ -541,9 +534,11 @@ func initRouter(r *gin.RouterGroup) { r.POST("/DeviceInfo/BindMqtt", deviceInfoApi.BindMqtt) r.POST("/DeviceInfo/BindTcp", deviceInfoApi.BindTcp) r.POST("/DeviceInfo/BindHTTP", deviceInfoApi.BindHTTP) + r.POST("/DeviceInfo/BindHCoap", deviceInfoApi.BindHCoap) r.GET("/DeviceInfo/QueryBindMqtt", deviceInfoApi.QueryBindMqtt) r.GET("/DeviceInfo/QueryBindTcp", deviceInfoApi.QueryBindTcp) r.GET("/DeviceInfo/QueryBindHTTP", deviceInfoApi.QueryBindHttp) + r.GET("/DeviceInfo/QueryBindCoap", deviceInfoApi.QueryBindCoap) r.POST("/ProductionPlan/create", productionPlanApi.CreateProductionPlan) r.POST("/ProductionPlan/update", productionPlanApi.UpdateProductionPlan) diff --git a/iot-go-project/models/models.go b/iot-go-project/models/models.go index 614bbd2..5fb768b 100644 --- a/iot-go-project/models/models.go +++ b/iot-go-project/models/models.go @@ -112,14 +112,14 @@ type Product struct { // DeviceInfo 设备信息 type DeviceInfo struct { ProductId uint `json:"product_id" structs:"product_id"` // 产品ID - ProductName string `gorm:"-" json:"product_name" structs:"product_name"` // 产品名称 + ProductName string `gorm:"-" json:"product_name,omitempty" ` // 产品名称 SN string `json:"sn" structs:"sn"` // 设备编号 ManufacturingDate *time.Time `json:"manufacturing_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"manufacturing_date"` // 制造日期 ProcurementDate *time.Time `json:"procurement_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"procurement_date"` // 采购日期 Source int `json:"source" structs:"source"` // 设备来源,1: 内部,2: 外源 WarrantyExpiry *time.Time `json:"warranty_expiry,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"warranty_expiry"` // 保修截止日期 - PushInterval int `json:"push_interval" structs:"push_interval"` // 推送间隔(秒) - ErrorRate float64 `json:"error_rate" structs:"error_rate"` // 推送时间误差(秒) + PushInterval int `json:"push_interval,omitempty" structs:"push_interval"` // 推送间隔(秒) + ErrorRate float64 `json:"error_rate,omitempty" structs:"error_rate"` // 推送时间误差(秒) gorm.Model `structs:"-"` } @@ -269,6 +269,16 @@ type HttpHandler struct { } +type CoapHandler struct { + DeviceInfoId uint `json:"device_info_id" structs:"device_info_id"` // 设备ID + Name string `json:"name" structs:"name"` // 处理器名 + Username string `json:"username" structs:"username"` // 用户名 + Password string `json:"password" structs:"password"` // 密码 + Script string `json:"script" structs:"script"` // 脚本 + gorm.Model `structs:"-"` +} + + type DeviceGroupBindMqttClient struct { gorm.Model `structs:"-"` DeviceGroupId uint `json:"device_group_id" structs:"device_group_id"` // 设备组ID diff --git a/iot-go-project/router/coap_handler_router.go b/iot-go-project/router/coap_handler_router.go new file mode 100644 index 0000000..9d45514 --- /dev/null +++ b/iot-go-project/router/coap_handler_router.go @@ -0,0 +1,181 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "igp/biz" + "igp/glob" + "igp/models" + "igp/servlet" + "strconv" +) + +type CoapHandlerApi struct{} + +var CoapHandlerBiz = biz.CoapHandlerBiz{} + +// CreateCoapHandler +// @Summary 创建Coap数据处理器 +// @Description 创建Coap数据处理器 +// @Tags CoapHandlers +// @Accept json +// @Produce json +// @Param CoapHandler body models.CoapHandler true "Coap数据处理器" +// @Success 201 {object} servlet.JSONResult{data=models.CoapHandler} "创建成功的Coap数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 500 {string} string "内部服务器错误" +// @Router /CoapHandler/create [post] +func (api *CoapHandlerApi) CreateCoapHandler(c *gin.Context) { + var CoapHandler models.CoapHandler + if err := c.ShouldBindJSON(&CoapHandler); err != nil { + servlet.Error(c, err.Error()) + return + } + + // 检查 CoapHandler 是否被正确初始化 + if CoapHandler.Name == "" { + servlet.Error(c, "名称不能为空") + return + } + + result := glob.GDb.Create(&CoapHandler) + + if result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + CoapHandlerBiz.SetRedis(CoapHandler) + // 返回创建成功的Coap数据处理器 + servlet.Resp(c, CoapHandler) +} + +// UpdateCoapHandler +// @Summary 更新一个Coap数据处理器 +// @Description 更新一个Coap数据处理器 +// @Tags CoapHandlers +// @Accept json +// @Produce json +// @Param CoapHandler body models.CoapHandler true "Coap数据处理器" +// @Success 200 {object} servlet.JSONResult{data=models.CoapHandler} "Coap数据处理器" +// @Failure 400 {string} string "请求数据错误" +// @Failure 404 {string} string "Coap数据处理器未找到" +// @Failure 500 {string} string "内部服务器错误" +// @Router /CoapHandler/update [post] +func (api *CoapHandlerApi) UpdateCoapHandler(c *gin.Context) { + var req models.CoapHandler + if err := c.ShouldBindJSON(&req); err != nil { + + servlet.Error(c, err.Error()) + return + } + + var old models.CoapHandler + result := glob.GDb.First(&old, req.ID) + if result.Error != nil { + + servlet.Error(c, "CoapHandler not found") + return + } + + var newV models.CoapHandler + newV = old + newV.Name = req.Name + newV.Script = req.Script + result = glob.GDb.Model(&newV).Updates(newV) + + if result.Error != nil { + + servlet.Error(c, result.Error.Error()) + return + } + CoapHandlerBiz.SetRedis(newV) + servlet.Resp(c, old) +} + +// PageCoapHandler +// @Summary 分页查询Coap数据处理器 +// @Description 分页查询Coap数据处理器 +// @Tags CoapHandlers +// @Accept json +// @Produce json +// @Param name query string false "Coap数据处理器名称" +// @Param pid query int false "上级id" +// @Param page query int false "页码" default(0) +// @Param page_size query int false "每页大小" default(10) +// @Success 200 {object} servlet.JSONResult{data=servlet.PaginationQ{data=models.CoapHandler}} "Coap数据处理器" +// @Failure 400 {string} string "请求参数错误" +// @Failure 500 {string} string "查询异常" +// @Router /CoapHandler/page [get] +func (api *CoapHandlerApi) PageCoapHandler(c *gin.Context) { + var name = c.Query("name") + var page = c.DefaultQuery("page", "0") + var pageSize = c.DefaultQuery("page_size", "10") + parseUint, err := strconv.Atoi(page) + if err != nil { + servlet.Error(c, "无效的页码") + return + } + u, err := strconv.Atoi(pageSize) + + if err != nil { + servlet.Error(c, "无效的页长") + return + } + + data, err := CoapHandlerBiz.PageData(name, parseUint, u) + if err != nil { + servlet.Error(c, "查询异常") + return + } + servlet.Resp(c, data) +} + +// DeleteCoapHandler +// @Tags CoapHandlers +// @Summary 删除Coap数据处理器 +// @Produce application/json +// @Param id path int true "主键" +// @Router /CoapHandler/delete/:id [post] +func (api *CoapHandlerApi) DeleteCoapHandler(c *gin.Context) { + var CoapHandler models.CoapHandler + + param := c.Param("id") + + result := glob.GDb.First(&CoapHandler, param) + if result.Error != nil { + servlet.Error(c, "CoapHandler not found") + + return + } + + if result := glob.GDb.Delete(&CoapHandler); result.Error != nil { + servlet.Error(c, result.Error.Error()) + return + } + CoapHandlerBiz.RemoveRedis(CoapHandler) + + servlet.Resp(c, "删除成功") +} + +// ByIdCoapHandler +// @Tags CoapHandlers +// @Summary 单个详情 +// @Param id path int true "主键" +// @Produce application/json +// @Router /CoapHandler/:id [get] +func (api *CoapHandlerApi) ByIdCoapHandler(c *gin.Context) { + var CoapHandler models.CoapHandler + + param := c.Param("id") + + result := glob.GDb.First(&CoapHandler, param) + if result.Error != nil { + servlet.Error(c, "CoapHandler not found") + + return + } + + servlet.Resp(c, CoapHandler) +} + + + diff --git a/iot-go-project/router/device_info_router.go b/iot-go-project/router/device_info_router.go index 5a7a600..ff4cc91 100644 --- a/iot-go-project/router/device_info_router.go +++ b/iot-go-project/router/device_info_router.go @@ -250,6 +250,27 @@ func (api *DeviceInfoApi) QueryBindHttp(c *gin.Context) { } servlet.Resp(c, res) } +// QueryBindCoap +// @Tags DeviceInfos +// @Summary 查询绑定HTTP客户端 +// @Accept json +// @Produce json +// @Param device_info_id path int true "主键" +// @Router /DeviceInfo/QueryBindCoap [get] +func (api *DeviceInfoApi) QueryBindCoap(c *gin.Context) { + param := c.Param("device_info_id") + + var res []models.CoapHandler + + // 使用 Where 和 Find 方法查询记录 + result := glob.GDb.Where("`device_info_id` = ?", param).Find(&res) + if result.Error != nil { + zap.S().Infoln("Error occurred during query:", result.Error) + servlet.Error(c, "暂无数据") + return + } + servlet.Resp(c, res) +} // QueryBindTcp // @Tags DeviceInfos @@ -445,10 +466,10 @@ func (api *DeviceInfoApi) BindTcp(c *gin.Context) { // BindHTTP // @Tags DeviceInfos -// @Summary 绑定tcp处理器 +// @Summary 绑定http处理器 // @Accept json // @Produce json -// @Param DeviceGroup body servlet.DeviceBindHTTPParam true "绑定参数" +// @Param DeviceGroup body models.HttpHandler true "绑定参数" // @Router /DeviceInfo/BindHTTP [post] func (api *DeviceInfoApi) BindHTTP(c *gin.Context) { var param models.HttpHandler @@ -489,7 +510,57 @@ func (api *DeviceInfoApi) BindHTTP(c *gin.Context) { } -func setHttpHandlerRedis(config models.HttpHandler){ +// BindHCoap +// @Tags DeviceInfos +// @Summary 绑定coap处理器 +// @Accept json +// @Produce json +// @Param DeviceGroup body models.CoapHandler true "绑定参数" +// @Router /DeviceInfo/BindHCoap [post] +func (api *DeviceInfoApi) BindHCoap(c *gin.Context) { + var param models.CoapHandler + if err := c.ShouldBindJSON(¶m); err != nil { + servlet.Error(c, err.Error()) + return + } + + if param.ID != 0 { + var old models.CoapHandler + + result := glob.GDb.First(&old, param.ID) + if result.Error != nil { + + servlet.Error(c, "HttpHandler not found") + return + } + var newV models.CoapHandler + newV = old + newV.DeviceInfoId = param.DeviceInfoId + newV.Name = param.Name + newV.Username = param.Username + newV.Password = param.Password + newV.Script = param.Script + // 更新记录 + result = glob.GDb.Model(&newV).Updates(newV) + setCoapHandlerRedis(newV) + } else { + // 新增 + glob.GDb.Model(models.CoapHandler{}).Create(¶m) + setCoapHandlerRedis(param) + + } + + glob.GRedis.LPush(context.Background(), "coap_bind_device_info:"+strconv.Itoa(int(param.ID)), param.DeviceInfoId) + + servlet.Resp(c, "绑定成功") + +} + +func setHttpHandlerRedis(config models.HttpHandler) { jsonData, _ := json.Marshal(config) - glob.GRedis.HSet(context.Background(), "auth:http",strconv.Itoa(int(config.DeviceInfoId)), jsonData) -} \ No newline at end of file + glob.GRedis.HSet(context.Background(), "auth:http", strconv.Itoa(int(config.DeviceInfoId)), jsonData) +} +func setCoapHandlerRedis(config models.CoapHandler) { + jsonData, _ := json.Marshal(config) + glob.GRedis.HSet(context.Background(), "auth:coap", strconv.Itoa(int(config.DeviceInfoId)), jsonData) +} diff --git a/protocol/coap/app-local.yml b/protocol/coap/app-local.yml new file mode 100644 index 0000000..ed0bbac --- /dev/null +++ b/protocol/coap/app-local.yml @@ -0,0 +1,16 @@ +node_info: + host: 127.0.0.1 + port: 5683 +redis_config: + host: 127.0.0.1 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest \ No newline at end of file diff --git a/protocol/coap/go.mod b/protocol/coap/go.mod index 6e45f02..87b951f 100644 --- a/protocol/coap/go.mod +++ b/protocol/coap/go.mod @@ -2,4 +2,17 @@ module iot-coap go 1.22.4 -require github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e +require ( + github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e + github.com/google/uuid v1.6.0 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.6.0 + go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/protocol/coap/go.sum b/protocol/coap/go.sum index 0250cb3..3883d89 100644 --- a/protocol/coap/go.sum +++ b/protocol/coap/go.sum @@ -1,2 +1,32 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e h1:oppjHFVTardH+VyOD32F9uBtgT5Wd/qVqEGcwj389Lc= github.com/dustin/go-coap v0.0.0-20190908170653-752e0f79981e/go.mod h1:as2rZ2aojRzZF8bGx1bPAn1yi9ICG6LwkiPOj6PBtjc= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= +github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/protocol/coap/main.go b/protocol/coap/main.go new file mode 100644 index 0000000..0b3be3f --- /dev/null +++ b/protocol/coap/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "errors" + "flag" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/yaml.v3" + "os" + "syscall" + "time" +) + +var globalConfig ServerConfig + +func main() { + var configPath string + flag.StringVar(&configPath, "config", "app-local.yml", "Path to the config file") + flag.Parse() + + yfile, err := os.ReadFile(configPath) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + initLog() + + err = yaml.Unmarshal(yfile, &globalConfig) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + zap.S().Infof("node name = %v , host = %v , port = %v", globalConfig.NodeInfo.Name, globalConfig.NodeInfo.Host, globalConfig.NodeInfo.Port) + InitRabbitCon() + initGlobalRedisClient(globalConfig.RedisConfig) + + Create(globalConfig.NodeInfo.Port) +} + + +var myTimeEncoder = zapcore.TimeEncoder(func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + // 按照 "2006-01-02 15:04:05" 的格式编码时间 + enc.AppendString(t.Format("2006-01-02 15:04:05")) +}) + +func initLog() { + encoderConfig := zapcore.EncoderConfig{ + // 使用自定义的时间编码器 + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stack", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码日志级别 + EncodeTime: myTimeEncoder, // 使用自定义的时间编码器 + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, // 短路径编码调用者 + } + + core := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), // 使用 Console 编码器 + zapcore.AddSync(os.Stdout), // 输出到标准输出 + zap.NewAtomicLevelAt(zap.InfoLevel), // 设置日志级别为 Debug + ) + + lg := zap.New(core, zap.AddCaller()) + zap.ReplaceGlobals(lg) // 替换全局 Logger + + // 确保日志被刷新 + defer func(lg *zap.Logger) { + err := lg.Sync() + if err != nil && !errors.Is(err, syscall.ENOTTY) { + zap.S().Errorf("日志同步失败 %+v", err) + } + }(lg) + + // 记录一条日志作为示例 + lg.Debug("这是一个调试级别的日志") +} +// ServerConfig 定义了服务器配置的结构体,包含了节点信息、Redis配置和消息队列配置。 +type ServerConfig struct { + // NodeInfo 定义了节点的信息,包括主机地址、端口、节点名称、节点类型和最大处理数量。 + NodeInfo NodeInfo `yaml:"node_info" json:"node_info"` + + // RedisConfig 定义了Redis服务器的配置,包括主机地址、端口、数据库索引和密码。 + RedisConfig RedisConfig `yaml:"redis_config" json:"redis_config"` + + // MQConfig 定义了消息队列服务器的配置,包括主机地址、端口、用户名和密码。 + MQConfig MQConfig `yaml:"mq_config" json:"mq_config"` +} + +// NodeInfo 定义了节点的基本信息。 +type NodeInfo struct { + // Host 表示节点的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示节点监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Name 表示节点的名称。 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Type 表示节点的类型。 + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // Size 表示节点可以处理的最大数量。 + Size int64 `json:"size,omitempty" yaml:"size,omitempty"` +} + +// RedisConfig 定义了Redis服务器的配置信息。 +type RedisConfig struct { + // Host 表示Redis服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示Redis服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Db 表示Redis服务器的数据库索引。 + Db int `json:"db,omitempty" yaml:"db,omitempty"` + + // Password 表示Redis服务器的访问密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} + +// MQConfig 定义了消息队列服务器的配置信息。 +type MQConfig struct { + // Host 表示消息队列服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示消息队列服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Username 表示用于访问消息队列服务器的用户名。 + Username string `json:"username,omitempty" yaml:"username,omitempty"` + + // Password 表示用于访问消息队列服务器的密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} diff --git a/protocol/coap/rabbit_mq.go b/protocol/coap/rabbit_mq.go new file mode 100644 index 0000000..a598756 --- /dev/null +++ b/protocol/coap/rabbit_mq.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "go.uber.org/zap" +) + +var GRabbitMq *amqp.Connection + +func CreateRabbitQueue(queueName string) { + + ch, err := GRabbitMq.Channel() + if err != nil { + zap.S().Fatalf("Failed to open a channel %v", err) + } + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _, err = ch.QueueDeclare(queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + zap.S().Fatalf("创建queue异常 %s", queueName) + } +} +func InitRabbitCon() { + conn, err := amqp.Dial(genUrl()) + if err != nil { + zap.S().Fatalf("Failed to connect to RabbitMQ %v", err) + } + + GRabbitMq = conn + + CreateRabbitQueue("pre_coap_handler") + +} +func genUrl() string { + connStr := fmt.Sprintf("amqp://%s:%s@%s:%d/", globalConfig.MQConfig.Username, globalConfig.MQConfig.Password, globalConfig.MQConfig.Host, globalConfig.MQConfig.Port) + return connStr +} + +// PushToQueue 将消息推送到RabbitMQ队列中 +// +// 参数: +// queue_name: string类型,目标队列的名称 +// body: []byte类型,待发送的消息体 +// +// 返回值: +// 无返回值 +func PushToQueue(queueName string, body []byte) { + + ch, _ := GRabbitMq.Channel() + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _ = ch.PublishWithContext(context.Background(), "", queueName, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: body, + }) + zap.S().Infof(" [x] 发送到 %s 消息体 %s", queueName, body) + +} diff --git a/protocol/coap/redis.go b/protocol/coap/redis.go new file mode 100644 index 0000000..2eeb794 --- /dev/null +++ b/protocol/coap/redis.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +var globalRedisClient *redis.Client + +func initGlobalRedisClient(config RedisConfig) { + + add := fmt.Sprintf("%s:%d", config.Host, config.Port) + globalRedisClient = redis.NewClient(&redis.Options{ + Addr: add, + Password: config.Password, // 如果没有设置密码,就留空字符串 + DB: config.Db, // 使用默认数据库 + }) + + // 检查连接是否成功 + if err := globalRedisClient.Ping(context.Background()).Err(); err != nil { + zap.S().Fatalf("Could not connect to Redis: %v", err) + } + +} diff --git a/protocol/coap/redis_lock.go b/protocol/coap/redis_lock.go new file mode 100644 index 0000000..bf10c5b --- /dev/null +++ b/protocol/coap/redis_lock.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "github.com/redis/go-redis/v9" + "sync" + "time" + + "github.com/google/uuid" +) + +const ( + LockTime = 4 * time.Second + RsDistlockNs = "tdln:" + ReleaseLockLua = ` + if redis.call('get',KEYS[1])==ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + ` +) + +type RedisDistLock struct { + id string + lockName string + redisClient *redis.Client + m sync.Mutex +} + +func NewRedisDistLock(redisClient *redis.Client, lockName string) *RedisDistLock { + return &RedisDistLock{ + lockName: lockName, + redisClient: redisClient, + } +} + +func (lock *RedisDistLock) Lock() { + for !lock.TryLock() { + time.Sleep(5 * time.Second) + } +} + +func (lock *RedisDistLock) TryLock() bool { + if lock.id != "" { + // 处于加锁中 + return false + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id != "" { + // 处于加锁中 + return false + } + ctx := context.Background() + id := uuid.New().String() + reply := lock.redisClient.SetNX(ctx, RsDistlockNs+lock.lockName, id, LockTime) + if reply.Err() == nil && reply.Val() { + lock.id = id + return true + } + + return false +} + +func (lock *RedisDistLock) Unlock() { + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + ctx := context.Background() + reply := lock.redisClient.Eval(ctx, ReleaseLockLua, []string{RsDistlockNs + lock.lockName}, lock.id) + if reply.Err() != nil { + panic("释放锁失败!") + } else { + lock.id = "" + } +} diff --git a/protocol/coap/sample/client/main.go b/protocol/coap/sample/client/main.go index 686606b..256b3f1 100644 --- a/protocol/coap/sample/client/main.go +++ b/protocol/coap/sample/client/main.go @@ -1,30 +1,73 @@ package main import ( + "encoding/json" "github.com/dustin/go-coap" "log" ) +type Auth struct { + Username string `json:"username"` + Password string `json:"password"` + DeviceId string `json:"device_id"` +} + func main() { + c, err := coap.Dial("udp", "localhost:5683") + if err != nil { + log.Fatalf("Error dialing: %v", err) + } + auth(c) + data(c) + +} +func data(c*coap.Conn) { req := coap.Message{ Type: coap.Confirmable, Code: coap.GET, MessageID: 12345, - Payload: []byte("hello, world!"), + Payload: []byte("test"), } - path := "/a" + path := "/data" req.SetOption(coap.ETag, "weetag") req.SetOption(coap.MaxAge, 3) req.SetPathString(path) - c, err := coap.Dial("udp", "localhost:5683") + + + rv, err := c.Send(req) if err != nil { - log.Fatalf("Error dialing: %v", err) + log.Fatalf("Error sending request: %v", err) } + if rv != nil { + log.Printf("Response payload: %s", rv.Payload) + } +} + +func auth( c *coap.Conn) { + auth := Auth{ + Username: "admin", + Password: "admin", + DeviceId: "1234567890", + } + marshal, _ := json.Marshal(auth) + req := coap.Message{ + Type: coap.Confirmable, + Code: coap.GET, + MessageID: 12345, + Payload: marshal, + } + + path := "/auth" + + req.SetOption(coap.ETag, "weetag") + req.SetOption(coap.MaxAge, 3) + req.SetPathString(path) + rv, err := c.Send(req) if err != nil { log.Fatalf("Error sending request: %v", err) @@ -33,5 +76,4 @@ func main() { if rv != nil { log.Printf("Response payload: %s", rv.Payload) } - -} \ No newline at end of file +} diff --git a/protocol/coap/server.go b/protocol/coap/server.go new file mode 100644 index 0000000..e51dcd6 --- /dev/null +++ b/protocol/coap/server.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/dustin/go-coap" + "go.uber.org/zap" + "log" + "net" +) + +func getUid(remoteAdd string) string { + val := globalRedisClient.HGet(context.Background(), "coap_uid_f", remoteAdd).Val() + return val +} + +func storageUid(uid, remoteAdd string) { + globalRedisClient.HSet(context.Background(), "coap_uid", uid, remoteAdd) + globalRedisClient.HSet(context.Background(), "coap_uid_f", remoteAdd, uid) +} +func RemoveUid(remoteAdd string) { + val := globalRedisClient.HGet(context.Background(), "coap_uid_f", remoteAdd).Val() + if val == "" { + return + } else { + globalRedisClient.HDel(context.Background(), "coap_uid", val) + + } +} + +func Create(port int) { + mux := coap.NewServeMux() + mux.Handle("/auth", coap.FuncHandler(auth)) //创建 "/auth"处理接口 + mux.Handle("/data", coap.FuncHandler(data)) //创建 "/data"处理接口 + log.Fatal(coap.ListenAndServe("udp", fmt.Sprintf(":%d", port), mux)) //启动Server 端口为5683 这里为什么要用":5683"?搞不明白 + +} + +type Auth struct { + Username string `json:"username"` + Password string `json:"password"` + DeviceId string `json:"device_id"` +} + +func auth(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message { + + if m.IsConfirmable() { + + auth := &Auth{} + err := json.Unmarshal(m.Payload, auth) + if err != nil { + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("安全认证信息异常"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + return res + } + + up, s := FindDeviceMappingUP(auth.DeviceId) + + if up != auth.Username || s != auth.Password { + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("安全认证信息异常"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + return res + } + + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("安全认证成功"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + storageUid(auth.DeviceId, a.String()) + return res + } + return nil +} + +func data(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message { + if m.IsConfirmable() { + uid := getUid(a.String()) + + if uid != "" { + coapMsg := CoapMessage{ + Uid: uid, + Message: string(m.Payload), + } + jsonData, err := json.Marshal(coapMsg) + if err != nil { + zap.S().Errorf("Error marshalling coap message to JSON: %v", err) + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("数据序列化异常"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + return res + } + PushToQueue("pre_coap_handler", jsonData) + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("数据处理成功"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + return res + } else { + res := &coap.Message{ + Type: coap.Acknowledgement, + Code: coap.Content, + MessageID: m.MessageID, + Token: m.Token, + Payload: []byte("你没有认证,请认证"), + } + res.SetOption(coap.ContentFormat, coap.TextPlain) + return res + } + } + return nil +} + +type CoapMessage struct { + Uid string `json:"uid"` + Message string `json:"message"` +} + +func FindDeviceMappingUP(deviceId string) (string, string) { + + i := globalRedisClient.Exists(context.Background(), "auth:coap").Val() + if i >= 0 { + + // todo: 从redis中根据deviceId获取用户名和密码 + val := globalRedisClient.HGet(context.Background(), "auth:coap", deviceId).Val() + var auth Auth + err := json.Unmarshal([]byte(val), &auth) + if err != nil { + zap.S().Errorf("Error unmarshalling auth to JSON: %v", err) + return "", "" + } + return auth.Username, auth.Password + } + return "", "" +} diff --git a/protocol/modbus/app-local.yml b/protocol/modbus/app-local.yml new file mode 100644 index 0000000..be293e2 --- /dev/null +++ b/protocol/modbus/app-local.yml @@ -0,0 +1,16 @@ +node_info: + host: 127.0.0.1 + port: 3332 +redis_config: + host: 127.0.0.1 + port: 6379 + db: 10 + password: eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + + + +mq_config: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest \ No newline at end of file diff --git a/protocol/modbus/go.mod b/protocol/modbus/go.mod index 488eebe..a39fe31 100644 --- a/protocol/modbus/go.mod +++ b/protocol/modbus/go.mod @@ -2,9 +2,18 @@ module iot-modbus go 1.22 -require github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2 +require ( + github.com/goburrow/modbus v0.1.0 + github.com/google/uuid v1.6.0 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.6.0 + github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2 + go.uber.org/zap v1.27.0 +) require ( - github.com/goburrow/modbus v0.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goburrow/serial v0.1.0 // indirect + go.uber.org/multierr v1.10.0 // indirect ) diff --git a/protocol/modbus/go.sum b/protocol/modbus/go.sum index 3caf70b..80ead67 100644 --- a/protocol/modbus/go.sum +++ b/protocol/modbus/go.sum @@ -1,6 +1,34 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro= github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg= github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= +github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2 h1:2H0HcvMX8JEa4HD32KJNBMwOBmCLs9xYOWVE8ig06Ss= github.com/tbrandon/mbserver v0.0.0-20231208015628-36eb59221ac2/go.mod h1:qUzPVlSj2UgxJkVbH0ZwuuiR46U8RBMDT5KLY78Ifpw= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/protocol/modbus/main.go b/protocol/modbus/main.go new file mode 100644 index 0000000..68eab3c --- /dev/null +++ b/protocol/modbus/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "flag" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + "os" +) +var globalConfig ServerConfig + +func main() { + var configPath string + flag.StringVar(&configPath, "config", "app-local.yml", "Path to the config file") + flag.Parse() + + yfile, err := os.ReadFile(configPath) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + err = yaml.Unmarshal(yfile, &globalConfig) + if err != nil { + zap.S().Fatalf("error: %v", err) + } + + zap.S().Infof("node name = %v , host = %v , port = %v", globalConfig.NodeInfo.Name, globalConfig.NodeInfo.Host, globalConfig.NodeInfo.Port) + InitRabbitCon() + + +} + + + + +// ServerConfig 定义了服务器配置的结构体,包含了节点信息、Redis配置和消息队列配置。 +type ServerConfig struct { + // NodeInfo 定义了节点的信息,包括主机地址、端口、节点名称、节点类型和最大处理数量。 + NodeInfo NodeInfo `yaml:"node_info" json:"node_info"` + + // RedisConfig 定义了Redis服务器的配置,包括主机地址、端口、数据库索引和密码。 + RedisConfig RedisConfig `yaml:"redis_config" json:"redis_config"` + + // MQConfig 定义了消息队列服务器的配置,包括主机地址、端口、用户名和密码。 + MQConfig MQConfig `yaml:"mq_config" json:"mq_config"` +} + +// NodeInfo 定义了节点的基本信息。 +type NodeInfo struct { + // Host 表示节点的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示节点监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Name 表示节点的名称。 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Type 表示节点的类型。 + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // Size 表示节点可以处理的最大数量。 + Size int64 `json:"size,omitempty" yaml:"size,omitempty"` +} + +// RedisConfig 定义了Redis服务器的配置信息。 +type RedisConfig struct { + // Host 表示Redis服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示Redis服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Db 表示Redis服务器的数据库索引。 + Db int `json:"db,omitempty" yaml:"db,omitempty"` + + // Password 表示Redis服务器的访问密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} + +// MQConfig 定义了消息队列服务器的配置信息。 +type MQConfig struct { + // Host 表示消息队列服务器的主机地址。 + Host string `json:"host,omitempty" yaml:"host,omitempty"` + + // Port 表示消息队列服务器监听的端口号。 + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // Username 表示用于访问消息队列服务器的用户名。 + Username string `json:"username,omitempty" yaml:"username,omitempty"` + + // Password 表示用于访问消息队列服务器的密码。 + Password string `json:"password,omitempty" yaml:"password,omitempty"` +} \ No newline at end of file diff --git a/protocol/modbus/rabbit_mq.go b/protocol/modbus/rabbit_mq.go new file mode 100644 index 0000000..a1cb6cf --- /dev/null +++ b/protocol/modbus/rabbit_mq.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "go.uber.org/zap" +) + +var GRabbitMq *amqp.Connection + +func CreateRabbitQueue(queueName string) { + + ch, err := GRabbitMq.Channel() + if err != nil { + zap.S().Fatalf("Failed to open a channel %v", err) + } + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _, err = ch.QueueDeclare(queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + zap.S().Fatalf("创建queue异常 %s", queueName) + } +} +func InitRabbitCon() { + conn, err := amqp.Dial(genUrl()) + if err != nil { + zap.S().Fatalf("Failed to connect to RabbitMQ %v", err) + } + + GRabbitMq = conn + + CreateRabbitQueue("pre_modbus_handler") + +} +func genUrl() string { + connStr := fmt.Sprintf("amqp://%s:%s@%s:%d/", globalConfig.MQConfig.Username, globalConfig.MQConfig.Password, globalConfig.MQConfig.Host, globalConfig.MQConfig.Port) + return connStr +} + +// PushToQueue 将消息推送到RabbitMQ队列中 +// +// 参数: +// queue_name: string类型,目标队列的名称 +// body: []byte类型,待发送的消息体 +// +// 返回值: +// 无返回值 +func PushToQueue(queueName string, body []byte) { + + ch, _ := GRabbitMq.Channel() + defer func(ch *amqp.Channel) { + err := ch.Close() + if err != nil { + zap.S().Errorf("Error: %+v", err) + + } + }(ch) + + _ = ch.PublishWithContext(context.Background(), "", queueName, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: body, + }) + zap.S().Infof(" [x] 发送到 %s 消息体 %s", queueName, body) + +} diff --git a/protocol/modbus/redis.go b/protocol/modbus/redis.go new file mode 100644 index 0000000..2eeb794 --- /dev/null +++ b/protocol/modbus/redis.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +var globalRedisClient *redis.Client + +func initGlobalRedisClient(config RedisConfig) { + + add := fmt.Sprintf("%s:%d", config.Host, config.Port) + globalRedisClient = redis.NewClient(&redis.Options{ + Addr: add, + Password: config.Password, // 如果没有设置密码,就留空字符串 + DB: config.Db, // 使用默认数据库 + }) + + // 检查连接是否成功 + if err := globalRedisClient.Ping(context.Background()).Err(); err != nil { + zap.S().Fatalf("Could not connect to Redis: %v", err) + } + +} diff --git a/protocol/modbus/redis_lock.go b/protocol/modbus/redis_lock.go new file mode 100644 index 0000000..bf10c5b --- /dev/null +++ b/protocol/modbus/redis_lock.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "github.com/redis/go-redis/v9" + "sync" + "time" + + "github.com/google/uuid" +) + +const ( + LockTime = 4 * time.Second + RsDistlockNs = "tdln:" + ReleaseLockLua = ` + if redis.call('get',KEYS[1])==ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + ` +) + +type RedisDistLock struct { + id string + lockName string + redisClient *redis.Client + m sync.Mutex +} + +func NewRedisDistLock(redisClient *redis.Client, lockName string) *RedisDistLock { + return &RedisDistLock{ + lockName: lockName, + redisClient: redisClient, + } +} + +func (lock *RedisDistLock) Lock() { + for !lock.TryLock() { + time.Sleep(5 * time.Second) + } +} + +func (lock *RedisDistLock) TryLock() bool { + if lock.id != "" { + // 处于加锁中 + return false + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id != "" { + // 处于加锁中 + return false + } + ctx := context.Background() + id := uuid.New().String() + reply := lock.redisClient.SetNX(ctx, RsDistlockNs+lock.lockName, id, LockTime) + if reply.Err() == nil && reply.Val() { + lock.id = id + return true + } + + return false +} + +func (lock *RedisDistLock) Unlock() { + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + lock.m.Lock() + defer lock.m.Unlock() + if lock.id == "" { + // 未加锁 + panic("解锁失败,因为未加锁") + } + ctx := context.Background() + reply := lock.redisClient.Eval(ctx, ReleaseLockLua, []string{RsDistlockNs + lock.lockName}, lock.id) + if reply.Err() != nil { + panic("释放锁失败!") + } else { + lock.id = "" + } +} diff --git a/protocol/tcp/server.go b/protocol/tcp/server.go index 82b4f16..86eb215 100644 --- a/protocol/tcp/server.go +++ b/protocol/tcp/server.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "encoding/json" "fmt" "go.uber.org/zap" @@ -12,11 +13,9 @@ import ( ) type Server struct { - host string - port string - deviceIdMap map[string]*Client - remoteIpMap map[string]string - mu sync.Mutex // 保护deviceIdMap的互斥锁 + host string + port string + mu sync.Mutex // 保护deviceIdMap的互斥锁 } type Client struct { @@ -30,10 +29,8 @@ type Config struct { func New(config *Config) *Server { return &Server{ - host: config.Host, - port: config.Port, - deviceIdMap: make(map[string]*Client), - remoteIpMap: make(map[string]string), + host: config.Host, + port: config.Port, } } @@ -64,10 +61,7 @@ func (server *Server) handleClient(client *Client) { server.mu.Lock() defer server.mu.Unlock() - // 更新或添加设备ID映射 - s := server.remoteIpMap[client.conn.RemoteAddr().String()] - delete(server.deviceIdMap, s) - delete(server.remoteIpMap, client.conn.RemoteAddr().String()) + RemoveUid(client.conn.RemoteAddr().String()) err := conn.Close() if err != nil { @@ -89,10 +83,10 @@ func (server *Server) handleMessage(client *Client, message string) { // 判断这个客户端是否建立过uid映射,没有的话不处理数据 - _, ok := server.remoteIpMap[client.conn.RemoteAddr().String()] + ok := getUid(client.conn.RemoteAddr().String()) - if ok { - handlerData(server,message, client) + if ok != "" { + handlerData(server, message, client) } else { deviceId := handlerUid(message) @@ -104,9 +98,8 @@ func (server *Server) handleMessage(client *Client, message string) { server.mu.Lock() defer server.mu.Unlock() - // 更新或添加设备ID映射 - server.deviceIdMap[deviceId] = client - server.remoteIpMap[client.conn.RemoteAddr().String()] = deviceId + storageUid(deviceId, client.conn.RemoteAddr().String()) + clientWrite(client, "成功识别设备编码.\n") return @@ -117,31 +110,48 @@ func (server *Server) handleMessage(client *Client, message string) { } +func getUid(remoteAdd string) string { + val := globalRedisClient.HGet(context.Background(), "tcp_uid_f", remoteAdd).Val() + return val +} + +func storageUid(uid, remoteAdd string) { + globalRedisClient.HSet(context.Background(), "tcp_uid", uid, remoteAdd) + globalRedisClient.HSet(context.Background(), "tcp_uid_f", remoteAdd, uid) +} +func RemoveUid(remoteAdd string) { + val := globalRedisClient.HGet(context.Background(), "tcp_uid_f", remoteAdd).Val() + if val == "" { + return + } else { + globalRedisClient.HDel(context.Background(), "tcp_uid", val) + + } +} + type TcpMessage struct { - Uid string `json:"uid"` + Uid string `json:"uid"` Message string `json:"message"` } func handlerData(server *Server, message string, client *Client) { - println(message) zap.S().Debugf("处理消息: %s 客户端: %s\n", message, client.conn.RemoteAddr().String()) - s := server.remoteIpMap[client.conn.RemoteAddr().String()] + s := getUid(client.conn.RemoteAddr().String()) - // 创建 MQTTMessage 实例并序列化为 JSON - mqttMsg := TcpMessage{ - Uid: s, - Message: message, + // 创建 TCPMessage 实例并序列化为 JSON + tcpMsg := TcpMessage{ + Uid: s, + Message: message, } - jsonData, err := json.Marshal(mqttMsg) + jsonData, err := json.Marshal(tcpMsg) if err != nil { zap.S().Errorf("Error marshalling TCP message to JSON: %v", err) return } PushToQueue("pre_tcp_handler", jsonData) - clientWrite(client, "数据已处理.\n") } -- Gitee From 3c35b922f14bcf9d55802ecf4b0715f1c01965b5 Mon Sep 17 00:00:00 2001 From: Zen Huifer Date: Thu, 25 Jul 2024 17:34:35 +0800 Subject: [PATCH 38/90] fix: update --- iot-go-project/biz/device_info_biz.go | 23 ++++++++++++++++----- iot-go-project/biz/production_plan_biz.go | 4 ++-- iot-go-project/initialize/init.go | 3 +-- iot-go-project/models/models.go | 7 +++---- iot-go-project/router/device_info_router.go | 17 +++++++++++---- iot-go-project/servlet/servlet.go | 23 +++++++++++++++++++++ 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/iot-go-project/biz/device_info_biz.go b/iot-go-project/biz/device_info_biz.go index f933ad7..81f60de 100644 --- a/iot-go-project/biz/device_info_biz.go +++ b/iot-go-project/biz/device_info_biz.go @@ -27,19 +27,32 @@ func (biz *DeviceInfoBiz) PageData(sn string, page, size int) (*servlet.Paginati offset := (page - 1) * size db.Offset(offset).Limit(size).Find(&dt) - for i, info := range dt { - dt[i].ProductName = productBiz.FindById(info.ProductId).Name - + var resp []servlet.DeviceInfoRes + + for _, info := range dt { + ProductName := productBiz.FindById(info.ProductId).Name + resp = append(resp, servlet.DeviceInfoRes{ + ProductId: info.ProductId, + SN: info.SN, + ManufacturingDate: &info.ManufacturingDate, + ProcurementDate: &info.ProcurementDate, + Source: info.Source, + WarrantyExpiry: &info.WarrantyExpiry, + PushInterval: info.PushInterval, + ErrorRate: info.ErrorRate, + Model: info.Model, + ProductName: ProductName, + }) } - pagination.Data = dt + pagination.Data = resp pagination.Page = page pagination.Size = size return &pagination, nil } -//mqttClients[i].LastPushTime = glob.GRedis.Get(context.Background(), "last_push_time:"+strconv.Itoa(int(client.ID))).Val() +//mqttClients[i].LastPushTime = glob.GRedis.Get(context.Background(), "last_push_time:"+strconv.Itoa(int(client.ID))).Val() func (biz *DeviceInfoBiz) FindById(id uint) *models.DeviceInfo { redis := biz.FindByIdWithRedis(id) diff --git a/iot-go-project/biz/production_plan_biz.go b/iot-go-project/biz/production_plan_biz.go index fa62871..fc96530 100644 --- a/iot-go-project/biz/production_plan_biz.go +++ b/iot-go-project/biz/production_plan_biz.go @@ -125,9 +125,9 @@ func (biz *ProductionPlanBiz) ChangeProductionPlanState(param servlet.Production info := models.DeviceInfo{ ProductId: plan.ProductID, SN: uuid.New().String(), // fixme: 生成设备SN - ManufacturingDate: &now, + ManufacturingDate: now, Source: 1, - WarrantyExpiry: &date, + WarrantyExpiry: date, } create := tx.Model(&models.DeviceInfo{}).Create(&info) diff --git a/iot-go-project/initialize/init.go b/iot-go-project/initialize/init.go index 83486ec..ffa3f83 100644 --- a/iot-go-project/initialize/init.go +++ b/iot-go-project/initialize/init.go @@ -18,7 +18,6 @@ import ( "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" - "igp/biz" "igp/glob" "igp/models" "igp/router" @@ -960,7 +959,7 @@ func InitAll(r *gin.RouterGroup) { initMongo() initRouter(r) - go biz.InitRedisExpireHandler(glob.GRedis) + //go biz.InitRedisExpireHandler(glob.GRedis) InitInfluxDbClient() } diff --git a/iot-go-project/models/models.go b/iot-go-project/models/models.go index 5fb768b..68dd1d2 100644 --- a/iot-go-project/models/models.go +++ b/iot-go-project/models/models.go @@ -112,12 +112,11 @@ type Product struct { // DeviceInfo 设备信息 type DeviceInfo struct { ProductId uint `json:"product_id" structs:"product_id"` // 产品ID - ProductName string `gorm:"-" json:"product_name,omitempty" ` // 产品名称 SN string `json:"sn" structs:"sn"` // 设备编号 - ManufacturingDate *time.Time `json:"manufacturing_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"manufacturing_date"` // 制造日期 - ProcurementDate *time.Time `json:"procurement_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"procurement_date"` // 采购日期 + ManufacturingDate time.Time `json:"manufacturing_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"manufacturing_date"` // 制造日期 + ProcurementDate time.Time `json:"procurement_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"procurement_date,omitempty"` // 采购日期 Source int `json:"source" structs:"source"` // 设备来源,1: 内部,2: 外源 - WarrantyExpiry *time.Time `json:"warranty_expiry,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"warranty_expiry"` // 保修截止日期 + WarrantyExpiry time.Time `json:"warranty_expiry,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"warranty_expiry"` // 保修截止日期 PushInterval int `json:"push_interval,omitempty" structs:"push_interval"` // 推送间隔(秒) ErrorRate float64 `json:"error_rate,omitempty" structs:"error_rate"` // 推送时间误差(秒) gorm.Model `structs:"-"` diff --git a/iot-go-project/router/device_info_router.go b/iot-go-project/router/device_info_router.go index ff4cc91..702058a 100644 --- a/iot-go-project/router/device_info_router.go +++ b/iot-go-project/router/device_info_router.go @@ -50,7 +50,7 @@ func (api *DeviceInfoApi) CreateDeviceInfo(c *gin.Context) { } if !DeviceInfo.ManufacturingDate.IsZero() { WarrantyExpiry := DeviceInfo.ManufacturingDate.AddDate(0, 0, Product.WarrantyPeriod) - DeviceInfo.WarrantyExpiry = &WarrantyExpiry + DeviceInfo.WarrantyExpiry = WarrantyExpiry } m := structs.Map(DeviceInfo) @@ -98,6 +98,13 @@ func (api *DeviceInfoApi) UpdateDeviceInfo(c *gin.Context) { var newV models.DeviceInfo newV = old + newV.Source = req.Source + newV.SN = req.SN + newV.ManufacturingDate = req.ManufacturingDate + newV.ProcurementDate = req.ProcurementDate + newV.WarrantyExpiry = req.WarrantyExpiry + newV.PushInterval = req.PushInterval + newV.ErrorRate = req.ErrorRate var Product models.Product result = glob.GDb.First(&Product, newV.ProductId) @@ -107,16 +114,18 @@ func (api *DeviceInfoApi) UpdateDeviceInfo(c *gin.Context) { } if !newV.ManufacturingDate.IsZero() { WarrantyExpiry := newV.ManufacturingDate.AddDate(0, 0, Product.WarrantyPeriod) - newV.WarrantyExpiry = &WarrantyExpiry + newV.WarrantyExpiry = WarrantyExpiry } result = glob.GDb.Model(&newV).Updates(newV) - deviceInfoBiz.SetRedis(newV) - if result.Error != nil { + if result.Error != nil { + zap.S().Errorw("更新 DeviceInfo 失败", "error", result.Error) servlet.Error(c, result.Error.Error()) return } + deviceInfoBiz.SetRedis(newV) + servlet.Resp(c, old) } diff --git a/iot-go-project/servlet/servlet.go b/iot-go-project/servlet/servlet.go index 1df70c1..e8d3456 100644 --- a/iot-go-project/servlet/servlet.go +++ b/iot-go-project/servlet/servlet.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" + "gorm.io/gorm" "iot-transmit/common" "net/http" "strings" @@ -320,3 +321,25 @@ type TransmitScriptParam struct { DataRowList []common.DataRowList `json:"data_row_list"` Script string `json:"script"` } + +type DeviceInfoRes struct { + ProductId uint `json:"product_id" structs:"product_id"` // 产品ID + + SN string `json:"sn" structs:"sn"` // 设备编号 + + ManufacturingDate *time.Time `json:"manufacturing_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"manufacturing_date"` // 制造日期 + + ProcurementDate *time.Time `json:"procurement_date,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"procurement_date"` // 采购日期 + + Source int `json:"source" structs:"source"` // 设备来源,1: 内部,2: 外源 + + WarrantyExpiry *time.Time `json:"warranty_expiry,omitempty" gorm:"type:DATETIME; default:NULL;" structs:"warranty_expiry"` // 保修截止日期 + + PushInterval int `json:"push_interval,omitempty" structs:"push_interval"` // 推送间隔(秒) + + ErrorRate float64 `json:"error_rate,omitempty" structs:"error_rate"` // 推送时间误差(秒) + + gorm.Model `structs:"-"` + ProductName string `gorm:"-" json:"product_name,omitempty" ` // 产品名称 + +} \ No newline at end of file -- Gitee From dc3c31026366d0ae7977503f58698a2ee247654f Mon Sep 17 00:00:00 2001 From: szw <1639914395@qq.com> Date: Thu, 25 Jul 2024 21:59:03 +0800 Subject: [PATCH 39/90] =?UTF-8?q?feat:=E8=AE=A1=E7=AE=97=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=92=8C=E8=84=9A=E6=9C=AC=E6=8A=A5=E8=AD=A6=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=A2=9E=E5=8A=A0=E8=A7=84=E5=88=99id?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ant-vue/src/i18n/en.ts | 4 +++- ant-vue/src/i18n/zh-CHS.ts | 4 +++- ant-vue/src/views/calculate-parameters/index.vue | 14 +++++++++++++- .../src/views/script-alarm-parameters/index.vue | 8 ++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ant-vue/src/i18n/en.ts b/ant-vue/src/i18n/en.ts index b9c76cc..cd1f30e 100644 --- a/ant-vue/src/i18n/en.ts +++ b/ant-vue/src/i18n/en.ts @@ -217,8 +217,10 @@ const en = { pleaseEnterIccid: 'Please Enter iccid', pleaseEnterAccessNumber: 'Please Enter Access Number', pleaseEnterExpiration: 'Please Enter Expiration', + pleaseCreateCalculateRule: 'Please create one calculation rule', + pleaseCreateScriptAlarmRule: 'Please create one script alarm rule', } }; -export default en; \ No newline at end of file +export default en; diff --git a/ant-vue/src/i18n/zh-CHS.ts b/ant-vue/src/i18n/zh-CHS.ts index f47eb98..5772423 100644 --- a/ant-vue/src/i18n/zh-CHS.ts +++ b/ant-vue/src/i18n/zh-CHS.ts @@ -217,7 +217,9 @@ const zhCHS = { pleaseEnterIccid: '请输入集成电路卡识别码', pleaseEnterAccessNumber: '请输入接入号', pleaseEnterExpiration: '请输入到期时间', + pleaseCreateCalculateRule: '请至少创建一条计算规则', + pleaseCreateScriptAlarmRule: '请至少创建一条脚本报警规则', } }; -export default zhCHS; \ No newline at end of file +export default zhCHS; diff --git a/ant-vue/src/views/calculate-parameters/index.vue b/ant-vue/src/views/calculate-parameters/index.vue index 5a76a27..35a2b6d 100644 --- a/ant-vue/src/views/calculate-parameters/index.vue +++ b/ant-vue/src/views/calculate-parameters/index.vue @@ -9,7 +9,7 @@ {{ $t('message.search') }} - {{ $t('message.addition') }} + {{ $t('message.addition') }}