瀏覽代碼

提交说明

luoxin 1 年之前
當前提交
2a73a858c1
共有 100 個文件被更改,包括 7003 次插入0 次删除
  1. 12 0
      .editorconfig
  2. 12 0
      .github/ISSUE_TEMPLATE/------.md
  3. 21 0
      .github/ISSUE_TEMPLATE/---bug--.md
  4. 25 0
      .github/workflows/maven.yml.bak
  5. 32 0
      .gitignore
  6. 48 0
      .gitmodules
  7. 二進制
      .mvn/wrapper/maven-wrapper.jar
  8. 1 0
      .mvn/wrapper/maven-wrapper.properties
  9. 125 0
      README.md
  10. 14 0
      build-and-push-docker.sh
  11. 3 0
      build.sh
  12. 28 0
      cq-fire/.gitignore
  13. 17 0
      cq-fire/Dockerfile
  14. 二進制
      cq-fire/config/font/FZFSK.TTF
  15. 二進制
      cq-fire/config/font/FZHTK.TTF
  16. 二進制
      cq-fire/config/font/FZKTK.TTF
  17. 二進制
      cq-fire/config/font/FZLTCXHJW.TTF
  18. 二進制
      cq-fire/config/font/FZXBSK.TTF
  19. 二進制
      cq-fire/config/font/YaHei.Consolas.1.12.ttf
  20. 二進制
      cq-fire/config/font/simfang.ttf
  21. 二進制
      cq-fire/config/font/simhei.ttf
  22. 二進制
      cq-fire/config/font/simkai.ttf
  23. 二進制
      cq-fire/config/font/simsun.ttc
  24. 61 0
      cq-fire/config/fop-configuration.xml
  25. 7 0
      cq-fire/docker-entrypoint.sh
  26. 392 0
      cq-fire/pom.xml
  27. 59 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/SmartFireApplication.java
  28. 31 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimension.java
  29. 58 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimensionProvider.java
  30. 21 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimensionType.java
  31. 66 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/LoginEvent.java
  32. 23 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/DeviceGatewayConfiguration.java
  33. 18 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/IndexPageWebFilter.java
  34. 26 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksConfiguration.java
  35. 70 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksProperties.java
  36. 42 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksRedisConfiguration.java
  37. 24 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/api/ApiInfoProperties.java
  38. 16 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/api/OpenApiConfiguration.java
  39. 38 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/doc/SwaggerConfiguration.java
  40. 53 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/fst/FstSerializationRedisSerializer.java
  41. 168 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ArchitectureDetailsInfo.java
  42. 109 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ArchitectureEntity.java
  43. 68 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/CategoryTestItemEntity.java
  44. 155 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/CorpEntity.java
  45. 103 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/NoticeEntity.java
  46. 113 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ReportEntity.java
  47. 81 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ServerAddressEntity.java
  48. 94 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/SystemConfEntity.java
  49. 122 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/TestItemEntity.java
  50. 128 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/UnitEntity.java
  51. 27 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/ApprovalStatusType.java
  52. 28 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpProcessStatus.java
  53. 26 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpStatus.java
  54. 26 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpType.java
  55. 28 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/Level.java
  56. 26 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/NoticeState.java
  57. 33 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/Operator.java
  58. 27 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/ReportTestState.java
  59. 27 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/SocialUnitProcessStatus.java
  60. 92 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/TestItemType.java
  61. 26 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/TestState.java
  62. 23 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/handle/CodeValidatedEvent.java
  63. 23 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/handle/VerifyCodeEvent.java
  64. 102 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/OrgInitService.java
  65. 97 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/architecture/ArchitectureExcelInfo.java
  66. 275 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/architecture/ArchitectureService.java
  67. 254 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/corp/CorpService.java
  68. 60 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/item/CategoryTestItemService.java
  69. 85 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/item/TestItemService.java
  70. 12 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/notice/NoticeService.java
  71. 520 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/report/ReportPdfInfo.java
  72. 519 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/report/ReportService.java
  73. 12 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/server/ServerAddressService.java
  74. 21 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/system/SystemConfService.java
  75. 90 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/term/DeviceModelTerm.java
  76. 180 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/unit/UnitExcelInfo.java
  77. 17 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/unit/UnitService.java
  78. 187 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/user/UserService.java
  79. 66 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestProvider.java
  80. 412 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestRule.java
  81. 215 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestTaskExecutor.java
  82. 24 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/ReportTestInfo.java
  83. 18 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/SubscriberProvider.java
  84. 22 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/TestItemCompletedEvent.java
  85. 23 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/util/Md5Util.java
  86. 32 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/util/RandomUtil.java
  87. 43 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/ClusterInfoController.java
  88. 31 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/SystemInfoController.java
  89. 76 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/api/ApiController.java
  90. 128 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/architecture/ArchitectureController.java
  91. 38 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/ChangePasswordCodeValidator.java
  92. 30 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/CodeVerifyRequest.java
  93. 146 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeController.java
  94. 18 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeInfo.java
  95. 75 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeProperties.java
  96. 30 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/ChangeProcessStatusRequest.java
  97. 27 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/ChangeStatusRequest.java
  98. 117 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/CorpController.java
  99. 48 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/item/CategoryTestItemController.java
  100. 57 0
      cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/item/TestItemController.java

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+
+[*.java]
+indent_style = space
+indent_size = tab
+tab_width = 4
+trim_trailing_whitespace = true
+insert_final_newline = false

+ 12 - 0
.github/ISSUE_TEMPLATE/------.md

@@ -0,0 +1,12 @@
+---
+name: "\U0001F451[需求]"
+about: 提交新需求
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**场景**
+ 
+**描述**

+ 21 - 0
.github/ISSUE_TEMPLATE/---bug--.md

@@ -0,0 +1,21 @@
+---
+name: "\U0001F41B[Bug] "
+about: 提交BUG
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**简要描述**
+
+**环境**
+
+Java版本:
+JetLinks版本:
+
+**复现步骤**
+
+**期望结果**
+ 
+**截图**

+ 25 - 0
.github/workflows/maven.yml.bak

@@ -0,0 +1,25 @@
+name: Push CI
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: self-hosted
+
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        token: '${{ secrets.CI_TOKEN }}'
+        submodules: true
+    - name: Set up JDK 1.8
+      uses: actions/setup-java@v1
+      with:
+        java-version: 1.8
+    - name: Cache Maven Repository
+      uses: actions/cache@v1
+      with:
+        path: ~/.m2
+        key: jetlinks-pro-maven-repository
+    - name: Build with Maven
+      run: ./mvnw -B package -DskipTests -Pbuild

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+**/pom.xml.versionsBackup
+**/target/
+**/out/
+*.class
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+.idea/
+/nbproject
+*.ipr
+*.iws
+*.iml
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.log
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+**/transaction-logs/
+!/.mvn/wrapper/maven-wrapper.jar
+/data/
+*.db
+/static/
+/upload
+/ui/upload/
+docker/data
+!ip2region.db
+!device-simulator.jar
+!demo-protocol-1.0.jar
+./dev/**
+/dist/data/

+ 48 - 0
.gitmodules

@@ -0,0 +1,48 @@
+[submodule "jetlinks-components"]
+	path = jetlinks-components
+	url = git@github.com:jetlinks/jetlinks-components.git
+	branch = master
+[submodule "jetlinks-parent"]
+	path = jetlinks-parent
+	url = git@github.com:jetlinks/jetlinks-parent.git
+	branch = master
+; [submodule "jetlinks-standalone"]
+; 	path = jetlinks-standalone
+; 	url = git@github.com:jetlinks/jetlinks-standalone.git
+; 	branch = master
+[submodule "jetlinks-manager/authentication-manager"]
+	path = jetlinks-manager/authentication-manager
+	url = git@github.com:jetlinks/authentication-manager.git
+	branch = master
+[submodule "jetlinks-manager/device-manager"]
+	path = jetlinks-manager/device-manager
+; 	url = git@github.com:jetlinks/device-manager.git
+    url = git@gitee.com:jetlinks-projects/jetlinks-fire-device-manager.git
+	branch = master
+[submodule "jetlinks-manager/rule-engine-manager"]
+	path = jetlinks-manager/rule-engine-manager
+	url = git@github.com:jetlinks/rule-engine-manager.git
+	branch = master
+[submodule "jetlinks-openapi"]
+	path = jetlinks-openapi
+	url = git@github.com:jetlinks/jetlinks-openapi.git
+	branch = master
+[submodule "jetlinks-manager/network-manager"]
+	path = jetlinks-manager/network-manager
+	url = git@github.com:jetlinks/network-manager.git
+	branch = master
+[submodule "jetlinks-manager/notify-manager"]
+	path = jetlinks-manager/notify-manager
+	url = git@github.com:jetlinks/notify-manager.git
+[submodule "jetlinks-manager/logging-manager"]
+	path = jetlinks-manager/logging-manager
+	url = git@github.com:jetlinks/logging-manager.git
+[submodule "jetlinks-manager/visualization-manager"]
+	path = jetlinks-manager/visualization-manager
+	url = git@github.com:jetlinks/visualization-manager.git
+[submodule "jetlinks-manager/datasource-manager"]
+	path = jetlinks-manager/datasource-manager
+	url = git@github.com:jetlinks/datasource-manager.git
+[submodule "expands-components/jetlinks-media"]
+	path = expands-components/jetlinks-media
+	url = git@github.com:jetlinks/jetlinks-media.git

二進制
.mvn/wrapper/maven-wrapper.jar


+ 1 - 0
.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1 @@
+distributionUrl=http://mirrors.hust.edu.cn/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip

+ 125 - 0
README.md

@@ -0,0 +1,125 @@
+# JetLinks PRO
+
+```bash
+---jetlinks-pro
+------|---jetlinks-components   # 组件库.
+------|-------|----common-component # 通用组件.
+------|-------|----dashboard-component # 仪表盘.
+------|-------|----elasticsearch-component # elasticsearch集成.
+------|-------|----gateway-component # 网关组件,消息网关,设备接入.
+------|-------|----io-component # IO 组件,Excel导入导出等.
+------|-------|----logging-component # 日志组件
+------|-------|----messaging-component # 消息中间件组件,RabbitMQ,Kafka等
+------|-------|----network-component # 网络组件,MQTT,TCP,CoAP,UDP等
+------|-------|----notify-component # 通知组件,短信,右键等通知
+------|-------|----protocol-component # 协议组件
+------|-------|----rule-engine-component # 规则引擎
+------|-------|----timeseries-component # 时序数据组件
+------|---jetlinks-manager  # 管理功能
+------|-------|----authentication-manager   # 用户,权限管理
+------|-------|----device-manager   # 设备管理
+------|-------|----logging-manager   # 日志管理
+------|-------|----network-manager   # 网络组件管理
+------|-------|----notify-manager   # 通知管理
+------|-------|----visualization-manager   # 数据可视化管理
+------|-------|----rule-engine-manager   # 规则引擎管理
+------|---jetlinks-openapi  #OpenAPI
+------|-------|----jetlinks-openapi-core    #OpenAPI核心模块
+------|-------|----jetlinks-openapi-manager    #OpenAPI管理
+------|---jetlinks-parent   # 父模块,统一依赖管理
+------|---simulator     # 模拟器
+
+```
+
+## 获取代码
+
+第一步: 先到个人设置中[添加SSH key](https://github.com/settings/keys)
+
+第二步: 拉取代码
+
+```bash
+ $ git clone --recurse-submodules git@github.com:jetlinks/jetlinks-pro.git && git submodule foreach git checkout master
+```
+
+第三步: 更新代码
+
+JetLinks Pro使用`git多模块`管理,使用此命令更新全部模块.
+```bash
+$ git pull && git submodule init && git submodule update && git submodule foreach git checkout master && git submodule foreach git pull origin master
+```
+
+添加代码到自建仓库(自行修改仓库地址):
+
+```bash
+$  git remote add gitee "git@gitee.com:/jetlinks/$(echo ${PWD##*/}).git"
+$  git submodule foreach 'git remote add gitee "git@gitee.com:/jetlinks/$(echo ${PWD##*/}).git"'
+$  git push gitee master
+$  git submodule foreach git push gitee master 
+```
+
+## 文档
+
+[查看文档](http://doc.jetlinks.cn/)
+
+
+## 开发
+
+开发之前,你应该对`java8`,`maven`,`spring-boot`,`reactor`,有一定了解.
+
+推荐使用Idea作为集成开发环境.
+
+推荐使用docker来快速启动完整的开发所需要的相关环境,比如:redis,postgresql,elasticsearch等.
+如果无法在开发环境中使用docker. 可使用内嵌方式启动开发环境.
+
+### docker方式启动开发环境
+
+直接在项目目录下运行命令即可:
+
+```bash
+
+$ docker-compose up -d
+
+```
+
+### 内嵌方式启动
+
+修改`jetlinks-standalone/src/main/resources/application.yml`中的环境配置.
+
+```bash
+spring:
+  profiles:
+    active: dev,embedded
+```
+
+或者修改Idea中的启动配置:
+
+![idea](idea-configuration.png)
+
+
+注意: 此方式默认会以内嵌方式启动redis,h2db,elasticsearch.可根据实际情况修改`application-embedded.yml`中
+的配置.
+
+## 部署
+
+项目发布基于`spring-boot`,可以使用spring-boot打成jar包的方式启动:
+
+```bash
+./mvnw clean package -Dmaven.test.skip=true
+```
+
+执行此命令成功后,`jetlinks-standalone/target/jetlinks-standalone.jar`则为可以直接运行的jar包.
+
+如果在docker环境下使用,可以构建成docker镜像:
+
+```bash
+
+#先打包
+./mvnw clean package -Dmaven.test.skip=true
+
+#构建docker镜像,可根据情况修改docker.image.name配置
+cd jetlinks-standalone
+../mvnw docker:build -Ddocker.image.name=jetlinks-pro
+
+```
+
+构建好镜像后可推送到自己到docker仓库中.

+ 14 - 0
build-and-push-docker.sh

@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+dockerImage=registry.cn-shenzhen.aliyuncs.com/jetlinks-projects/cq-smart-fire:230324     #230209      #1116
+mvn clean package \
+-Dmaven.test.skip=true \
+-Dmaven.build.timestamp="$(date"+%Y-%m-%d%H:%M:%S")"
+#if [ $? -ne 0 ];then
+#    echo "构建失败!"
+#else
+#  cd ./cq-fire || exit
+##  docker build -t "$dockerImage"
+#  docker build -t "$dockerImage" . && docker push "$dockerImage"
+#fi
+

+ 3 - 0
build.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+./mvnw clean package -Dmaven.test.skip=true -Dmaven.build.timestamp="$(date "+%Y-%m-%d %H:%M:%S")"

+ 28 - 0
cq-fire/.gitignore

@@ -0,0 +1,28 @@
+**/pom.xml.versionsBackup
+**/target/
+**/out/
+*.class
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+.idea/
+/nbproject
+*.ipr
+*.iws
+*.iml
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.log
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+**/transaction-logs/
+!/.mvn/wrapper/maven-wrapper.jar
+/data/
+*.db
+/static/
+/upload
+/ui/upload/
+docker/data
+**/application-local.yml

+ 17 - 0
cq-fire/Dockerfile

@@ -0,0 +1,17 @@
+FROM openjdk:8u272-jdk as builder
+WORKDIR application
+ARG JAR_FILE=target/cq-fire.jar
+COPY ${JAR_FILE} application.jar
+RUN java -Djarmode=layertools -jar application.jar extract
+
+FROM openjdk:8u272-jdk
+WORKDIR application
+COPY --from=builder application/dependencies/ ./
+COPY --from=builder application/snapshot-dependencies/ ./
+COPY --from=builder application/spring-boot-loader/ ./
+COPY --from=builder application/application/ ./
+COPY docker-entrypoint.sh ./
+COPY config ./config
+COPY config/font/ /usr/share/fonts/truetype/
+RUN chmod +x docker-entrypoint.sh
+ENTRYPOINT ["./docker-entrypoint.sh"]

二進制
cq-fire/config/font/FZFSK.TTF


二進制
cq-fire/config/font/FZHTK.TTF


二進制
cq-fire/config/font/FZKTK.TTF


二進制
cq-fire/config/font/FZLTCXHJW.TTF


二進制
cq-fire/config/font/FZXBSK.TTF


二進制
cq-fire/config/font/YaHei.Consolas.1.12.ttf


二進制
cq-fire/config/font/simfang.ttf


二進制
cq-fire/config/font/simhei.ttf


二進制
cq-fire/config/font/simkai.ttf


二進制
cq-fire/config/font/simsun.ttc


+ 61 - 0
cq-fire/config/fop-configuration.xml

@@ -0,0 +1,61 @@
+<fop version="1.0">
+    <!-- Strict user configuration -->
+    <strict-configuration>true</strict-configuration>
+
+    <!-- Strict FO validation -->
+    <strict-validation>true</strict-validation>
+
+    <!-- Base URL for resolving relative URLs -->
+    <base>./config</base>
+
+    <!-- Font Base URL for resolving relative font URLs -->
+    <font-base>./config/font</font-base>
+
+    <!-- Source resolution in dpi (dots/pixels per inch) for determining the size of pixels in SVG and bitmap images, default: 72dpi -->
+    <source-resolution>72</source-resolution>
+    <!-- Target resolution in dpi (dots/pixels per inch) for specifying the target resolution for generated bitmaps, default: 72dpi -->
+    <target-resolution>72</target-resolution>
+
+    <!-- default page-height and page-width, in case
+         value is specified as auto -->
+    <default-page-settings height="11in" width="8.26in"/>
+
+    <!-- Use file name nl_Bel instead of the default nl_BE -->
+    <hyphenation-pattern lang="nl" country="BE">nl_Bel</hyphenation-pattern>
+
+    <!-- etc. etc..... -->
+    <fonts>
+        <font kerning="yes" embed-url="config/font/YaHei.Consolas.1.12.ttf">
+            <font-triplet name="YaHei Consolas Hybrid" style="normal" weight="normal"/>
+            <font-triplet name="Microsoft YaHei" style="normal" weight="normal"/>
+            <font-triplet name="微软雅黑" style="normal" weight="normal"/>
+        </font>
+        <font kerning="yes" embed-url="config/font/FZLTCXHJW.TTF">
+            <font-triplet name="FZLanTingHeiS" style="normal" weight="normal"/>
+            <font-triplet name="方正兰亭超细黑简体" style="normal" weight="normal"/>
+        </font>
+        <font kerning="yes" embed-url="config/font/simfang.ttf">
+            <font-triplet name="宋体" style="normal" weight="normal"/>
+            <font-triplet name="仿宋" style="normal" weight="normal"/>
+            <font-triplet name="仿宋_GB2312" style="normal" weight="normal"/>
+
+        </font>
+        <font kerning="yes" embed-url="config/font/simhei.ttf">
+            <font-triplet name="黑体" style="normal" weight="normal"/>
+        </font>
+        <font kerning="yes" embed-url="config/font/simkai.ttf">
+            <font-triplet name="楷体" style="normal" weight="normal"/>
+            <font-triplet name="楷体_GB2312" style="normal" weight="normal"/>
+        </font>
+
+        <font kerning="yes" embed-url="config/font/FZFSK.TTF">
+            <font-triplet name="方正仿宋" style="normal" weight="normal"/>
+            <font-triplet name="方正仿宋_GBK" style="normal" weight="normal"/>
+        </font>
+
+        <font kerning="yes" embed-url="config/font/FZXBSK.TTF">
+            <font-triplet name="方正小标宋" style="normal" weight="normal"/>
+            <font-triplet name="方正小标宋_GBK" style="normal" weight="normal"/>
+        </font>
+    </fonts>
+</fop>

+ 7 - 0
cq-fire/docker-entrypoint.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+java $JAVA_OPTS -server \
+-XX:+UnlockExperimentalVMOptions \
+-XX:+UseCGroupMemoryLimitForHeap \
+-XX:-OmitStackTraceInFastThrow \
+-Djava.security.egd=file:/dev/./urandom \
+org.springframework.boot.loader.JarLauncher

+ 392 - 0
cq-fire/pom.xml

@@ -0,0 +1,392 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.jetlinks.pro</groupId>
+        <artifactId>jetlinks-parent</artifactId>
+        <version>1.20.0-SNAPSHOT</version>
+        <relativePath>../jetlinks-parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>cq-fire</artifactId>
+
+    <properties>
+        <docker.image.name>registry.cn-shenzhen.aliyuncs.com/jetlinks-pro/${project.artifactId}</docker.image.name>
+        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>x86_64</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <dependencies>
+
+                <dependency>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-transport-native-epoll</artifactId>
+                    <classifier>linux-x86_64</classifier>
+                </dependency>
+
+                <dependency>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-transport-native-kqueue</artifactId>
+                    <classifier>osx-x86_64</classifier>
+                </dependency>
+
+            </dependencies>
+        </profile>
+
+        <profile>
+            <id>aarch64</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-transport-native-epoll</artifactId>
+                    <classifier>linux-aarch64</classifier>
+                </dependency>
+            </dependencies>
+        </profile>
+
+        <profile>
+            <id>cassandra</id>
+            <dependencies>
+                <dependency>
+                    <groupId>org.jetlinks.pro</groupId>
+                    <artifactId>cassandra-component</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+            </dependencies>
+        </profile>
+
+    </profiles>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <artifactId>maven-install-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>org.jetlinks.pro.cqfire.SmartFireApplication</mainClass>
+                    <layout>ZIP</layout>
+                    <layers>
+                        <enabled>true</enabled>
+                    </layers>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-printer-core</artifactId>
+            <version>2.1.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>datasource-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>kafka-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>rabbitmq-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-tools</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>dashboard-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>logging-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gateway-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>protocol-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>rule-engine-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>visualization-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>jetlinks-openapi-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>logging-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>authentication-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>network-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>device-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>geo-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>notify-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>notify-dingtalk</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>notify-wechat</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>notify-network</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-supports</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!--        <dependency>-->
+        <!--            <groupId>org.springframework.boot</groupId>-->
+        <!--            <artifactId>spring-boot-starter-actuator</artifactId>-->
+        <!--        </dependency>-->
+
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
+        </dependency>
+
+
+        <dependency>
+            <groupId>dev.miku</groupId>
+            <artifactId>r2dbc-mysql</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-postgresql</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.projectreactor.netty</groupId>
+            <artifactId>reactor-netty</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-h2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-authorization-basic</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.elasticsearch.client</groupId>
+            <artifactId>elasticsearch-rest-high-level-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-system-file</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-system-dictionary</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-access-logging-aop</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>de.ruedigermoeller</groupId>
+            <artifactId>fst</artifactId>
+            <version>2.57</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>influxdb-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>tdengine-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>clickhouse-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-webflux-ui</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-springdoc-ui</artifactId>
+            <version>3.0.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>CQ-GB26875-protocol</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>jetlinks-media</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetlinks.pro</groupId>
+            <artifactId>test-component</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.belerweb</groupId>
+            <artifactId>pinyin4j</artifactId>
+            <version>2.5.1</version>
+        </dependency>
+
+        <!--        <dependency>-->
+        <!--            <groupId>com.github.xiaoymin</groupId>-->
+        <!--            <artifactId>knife4j-spring-boot-starter</artifactId>-->
+        <!--            <version>2.0.5</version>-->
+        <!--        </dependency>-->
+    </dependencies>
+</project>

+ 59 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/SmartFireApplication.java

@@ -0,0 +1,59 @@
+package org.jetlinks.pro.cqfire;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.authorization.basic.configuration.EnableAopAuthorize;
+import org.hswebframework.web.authorization.events.AuthorizingHandleBeforeEvent;
+import org.hswebframework.web.crud.annotation.EnableEasyormRepository;
+import org.hswebframework.web.logging.aop.EnableAccessLogger;
+import org.hswebframework.web.logging.events.AccessLoggerAfterEvent;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration;
+import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+
+@SpringBootApplication(scanBasePackages = "org.jetlinks", exclude = {
+    DataSourceAutoConfiguration.class,
+    KafkaAutoConfiguration.class,
+    RabbitAutoConfiguration.class,
+    ElasticsearchRestClientAutoConfiguration.class,
+    ElasticsearchDataAutoConfiguration.class
+})
+@EnableCaching
+@EnableEasyormRepository("org.jetlinks.pro.**.entity")
+@EnableAopAuthorize
+@EnableAccessLogger
+//@EnableReactorQL("org.jetlinks.pro")
+public class SmartFireApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(SmartFireApplication.class, args);
+    }
+
+    @Component
+    @Slf4j
+    public static class AdminAllAccess {
+
+        @EventListener
+        public void handleAuthEvent(AuthorizingHandleBeforeEvent e) {
+            if (e.getContext().getAuthentication().getUser().getUsername().equals("admin")) {
+                e.setAllow(true);
+            }
+        }
+
+        @EventListener
+        public void handleAccessLogger(AccessLoggerAfterEvent event) {
+
+            log.info("{}=>{} {}-{}", event.getLogger().getIp(), event.getLogger().getUrl(), event.getLogger().getDescribe(), event.getLogger().getAction());
+
+        }
+    }
+
+
+}

+ 31 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimension.java

@@ -0,0 +1,31 @@
+package org.jetlinks.pro.cqfire.authorize;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionType;
+
+import java.util.Map;
+
+@Getter
+@Setter
+public class CorpDimension implements Dimension {
+    private String id;
+
+    private String name;
+
+    private Map<String,Object> options;
+    @Override
+    public DimensionType getType() {
+        return CorpDimensionType.INSTANCE;
+    }
+
+    public static CorpDimension of(String id, String name){
+        CorpDimension dimension=new CorpDimension();
+
+        dimension.setId(id);
+        dimension.setName(name);
+        return dimension;
+
+    }
+}

+ 58 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimensionProvider.java

@@ -0,0 +1,58 @@
+package org.jetlinks.pro.cqfire.authorize;
+
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.DimensionProvider;
+import org.hswebframework.web.authorization.DimensionType;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.pro.cqfire.entity.CorpEntity;
+import org.jetlinks.pro.cqfire.service.corp.CorpService;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Component
+public class CorpDimensionProvider implements DimensionProvider {
+
+    private final CorpService corpService;
+
+    public CorpDimensionProvider(CorpService corpService) {
+        this.corpService = corpService;
+    }
+
+    @Override
+    public Flux<? extends DimensionType> getAllType() {
+        return Flux.just(CorpDimensionType.INSTANCE);
+    }
+
+    @Override
+    public Flux<? extends Dimension> getDimensionByUserId(String userId) {
+
+        return corpService.createQuery()
+                          .where()
+                          .and(CorpEntity::getCreatorId, userId)
+                          .fetch()
+                          .map(CorpEntity::toDimension)
+            ;
+    }
+
+    @Override
+    public Mono<? extends Dimension> getDimensionById(DimensionType type, String id) {
+        if (!type.isSameType(CorpDimensionType.INSTANCE)) {
+            return Mono.empty();
+        }
+        return corpService
+            .findById(id)
+            .map(CorpEntity::toDimension)
+            ;
+    }
+
+    @Override
+    public Flux<String> getUserIdByDimensionId(String dimensionId) {
+        return corpService
+            .findById(dimensionId)
+            .map(CorpEntity::getCreatorId)
+            .flux()
+            ;
+    }
+}

+ 21 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/CorpDimensionType.java

@@ -0,0 +1,21 @@
+package org.jetlinks.pro.cqfire.authorize;
+
+import org.hswebframework.web.authorization.DimensionType;
+
+import java.io.Serializable;
+
+public class CorpDimensionType implements DimensionType, Serializable {
+    private static final long serialVersionUID = -6849794470754667710L;
+
+    public static CorpDimensionType INSTANCE = new CorpDimensionType();
+
+    @Override
+    public String getId() {
+        return "corp";
+    }
+
+    @Override
+    public String getName() {
+        return "企业";
+    }
+}

+ 66 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/authorize/LoginEvent.java

@@ -0,0 +1,66 @@
+package org.jetlinks.pro.cqfire.authorize;
+
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.Permission;
+import org.hswebframework.web.authorization.events.AuthorizationSuccessEvent;
+import org.hswebframework.web.authorization.exception.AccessDenyException;
+import org.hswebframework.web.authorization.exception.UnAuthorizedException;
+import org.jetlinks.pro.auth.service.UserDetailService;
+import org.jetlinks.pro.cqfire.service.corp.CorpService;
+import org.jetlinks.pro.cqfire.web.user.CorpUserDetail;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Lind
+ * @since 1.0.0
+ */
+@Component
+public class LoginEvent {
+
+    private final UserDetailService detailService;
+
+    private final CorpService corpService;
+
+    public LoginEvent(UserDetailService detailService, CorpService corpService) {
+        this.detailService = detailService;
+        this.corpService = corpService;
+    }
+
+    @EventListener
+    public void handleLoginSuccess(AuthorizationSuccessEvent event) {
+        Map<String, Object> result = event.getResult();
+        Authentication authentication = event.getAuthentication();
+        List<Dimension> dimensions = authentication.getDimensions();
+
+        result.put("permissions", authentication.getPermissions());
+        result.put("roles", dimensions);
+        result.put("currentAuthority", authentication
+            .getPermissions()
+            .stream()
+            .map(Permission::getId)
+            .collect(Collectors.toList()));
+        event.async(
+            corpService.getCorpUserDetailByUserId(authentication.getUser().getId())
+                .map(CorpUserDetail::getProcessStatus)
+                .doOnNext(status -> result.put("corp", status))
+        );
+        event.async(
+            detailService
+                .findUserDetail(event.getAuthentication().getUser().getId())
+                .doOnNext(detail -> {
+                    result.put("user", detail);
+                    //租户已禁用
+                    if (detail.isTenantDisabled()) {
+                        throw new AccessDenyException("用户已禁用");
+                    }
+                })
+        );
+    }
+}

+ 23 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/DeviceGatewayConfiguration.java

@@ -0,0 +1,23 @@
+package org.jetlinks.pro.cqfire.configuration;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.VertxOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DeviceGatewayConfiguration {
+
+    @ConfigurationProperties(prefix = "vertx")
+    @Bean
+    public VertxOptions vertxOptions() {
+        return new VertxOptions();
+    }
+
+    @Bean
+    public Vertx vertx(VertxOptions vertxOptions) {
+        return Vertx.vertx(vertxOptions);
+    }
+
+}

+ 18 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/IndexPageWebFilter.java

@@ -0,0 +1,18 @@
+package org.jetlinks.pro.cqfire.configuration;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+@Component
+public class IndexPageWebFilter implements WebFilter {
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+        if (exchange.getRequest().getURI().getPath().equals("/")) {
+            return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()).build());
+        }
+        return chain.filter(exchange);
+    }
+}

+ 26 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksConfiguration.java

@@ -0,0 +1,26 @@
+package org.jetlinks.pro.cqfire.configuration;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@Slf4j
+public class JetLinksConfiguration {
+
+    @Bean
+    public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> webServerFactoryWebServerFactoryCustomizer() {
+        //解决请求参数最大长度问题
+        return factory -> factory
+            .addServerCustomizers(httpServer -> httpServer
+                .httpRequestDecoder(spec -> {
+                    spec.maxInitialLineLength(10240);
+                    spec.maxHeaderSize(10240);
+                    return spec;
+                }));
+    }
+
+
+}

+ 70 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksProperties.java

@@ -0,0 +1,70 @@
+package org.jetlinks.pro.cqfire.configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.jetlinks.supports.cluster.event.RSocketAddress;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import javax.annotation.PostConstruct;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+//已弃用
+@ConfigurationProperties(prefix = "jetlinks")
+@Getter
+@Setter
+@Deprecated
+public class JetLinksProperties {
+
+    private String serverId;
+
+    private String clusterName = "default";
+
+    private Map<String, Long> transportLimit;
+
+    private EventBusProperties eventBus = new EventBusProperties();
+
+    @Getter
+    @Setter
+    public static class EventBusProperties {
+
+        RSocketEventBusProperties rsocket = new RSocketEventBusProperties();
+
+        ClusterProperties cluster = new ClusterProperties();
+
+    }
+
+    @Getter
+    @Setter
+    public static class ClusterProperties {
+
+        private boolean enabled = false;
+
+        private List<String> seeds = new ArrayList<>();
+
+        private int port;
+
+        private String host;
+    }
+
+    @Getter
+    @Setter
+    public static class RSocketEventBusProperties {
+
+        private boolean enabled = false;
+
+        private RSocketAddress address = new RSocketAddress();
+
+    }
+
+    @PostConstruct
+    @SneakyThrows
+    public void init() {
+        if (serverId == null) {
+            serverId = InetAddress.getLocalHost().getHostName();
+        }
+    }
+}

+ 42 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/JetLinksRedisConfiguration.java

@@ -0,0 +1,42 @@
+package org.jetlinks.pro.cqfire.configuration;
+
+import org.jetlinks.pro.cqfire.configuration.fst.FstSerializationRedisSerializer;
+import org.nustaq.serialization.FSTConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@ConditionalOnProperty(prefix = "spring.redis",name = "serializer",havingValue = "fst")
+public class JetLinksRedisConfiguration {
+
+    @Bean
+    public ReactiveRedisTemplate<Object, Object> reactiveRedisTemplate(
+        ReactiveRedisConnectionFactory reactiveRedisConnectionFactory, ResourceLoader resourceLoader) {
+
+        FstSerializationRedisSerializer serializer = new FstSerializationRedisSerializer(() -> {
+                FSTConfiguration configuration = FSTConfiguration.createDefaultConfiguration()
+                    .setForceSerializable(true);
+                configuration.setClassLoader(resourceLoader.getClassLoader());
+                return configuration;
+            });
+
+        @SuppressWarnings("all")
+        RedisSerializationContext<Object, Object> serializationContext = RedisSerializationContext
+            .newSerializationContext()
+            .key((RedisSerializer) new StringRedisSerializer())
+            .value(serializer)
+            .hashKey(StringRedisSerializer.UTF_8)
+            .hashValue(serializer)
+            .build();
+
+        return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, serializationContext);
+    }
+
+}

+ 24 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/api/ApiInfoProperties.java

@@ -0,0 +1,24 @@
+package org.jetlinks.pro.cqfire.configuration.api;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Getter
+@Setter
+@ConfigurationProperties(prefix = "api")
+@Component
+public class ApiInfoProperties {
+
+    @Schema(description = "api根路径")
+    private String basePath;
+
+    @Schema(description = "api地址信息")
+    private Map<String, String> urls = new ConcurrentHashMap<>();
+
+}

+ 16 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/api/OpenApiConfiguration.java

@@ -0,0 +1,16 @@
+package org.jetlinks.pro.cqfire.configuration.api;
+
+import org.jetlinks.pro.openapi.OpenApiClientManager;
+import org.jetlinks.pro.openapi.interceptor.OpenApiFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+public class OpenApiConfiguration {
+
+    @Bean
+    public OpenApiFilter openApiFilter(OpenApiClientManager clientManager) {
+        return new OpenApiFilter(clientManager);
+    }
+
+}

+ 38 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/doc/SwaggerConfiguration.java

@@ -0,0 +1,38 @@
+package org.jetlinks.pro.cqfire.configuration.doc;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.info.Contact;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import io.swagger.v3.oas.annotations.security.SecuritySchemes;
+import org.springdoc.webflux.core.SpringDocWebFluxConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@OpenAPIDefinition(
+    info = @Info(
+        title = "物联网平台",
+        description = "物联网平台接口文档",
+        contact = @Contact(name = "admin"),
+        version = "2.0.0-SNAPSHOT"
+    )
+)
+@SecuritySchemes(
+    {
+        @SecurityScheme(
+            type = SecuritySchemeType.HTTP,
+            name = "Token",
+            paramName = "X-Access-Token",
+            in = SecuritySchemeIn.HEADER,
+            description = "认证token"
+        )
+    }
+)
+@AutoConfigureBefore(SpringDocWebFluxConfiguration.class)
+public class SwaggerConfiguration {
+
+
+}

+ 53 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/configuration/fst/FstSerializationRedisSerializer.java

@@ -0,0 +1,53 @@
+package org.jetlinks.pro.cqfire.configuration.fst;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.nustaq.serialization.FSTConfiguration;
+import org.nustaq.serialization.FSTObjectInput;
+import org.nustaq.serialization.FSTObjectOutput;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.io.ByteArrayOutputStream;
+import java.util.function.Supplier;
+
+@AllArgsConstructor
+public class FstSerializationRedisSerializer implements RedisSerializer<Object> {
+
+    private final FastThreadLocal<FSTConfiguration> configuration;
+
+    public FstSerializationRedisSerializer(Supplier<FSTConfiguration> supplier) {
+
+        this(new FastThreadLocal<FSTConfiguration>() {
+            @Override
+            protected FSTConfiguration initialValue() {
+                return supplier.get();
+            }
+        });
+    }
+
+    @Override
+    @SneakyThrows
+    public byte[] serialize(Object o) throws SerializationException {
+        if (o == null) {
+            return null;
+        }
+        ByteArrayOutputStream arr = new ByteArrayOutputStream(1024);
+        try (FSTObjectOutput output = configuration.get().getObjectOutput(arr)) {
+            output.writeObject(o);
+        }
+        return arr.toByteArray();
+    }
+
+    @Override
+    @SneakyThrows
+    public Object deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null) {
+            return null;
+        }
+        try (FSTObjectInput input = configuration.get().getObjectInput(bytes)) {
+            return input.readObject();
+        }
+    }
+}

+ 168 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ArchitectureDetailsInfo.java

@@ -0,0 +1,168 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * @author gyl
+ * @version 1.0
+ * @time 2022/2/22 11:17
+ */
+@Data
+public class ArchitectureDetailsInfo {
+    @Schema(description = "街道")
+    private String street;
+
+    @Schema(description = "街道id")
+    private String streetId;
+
+    @Schema(description = "建筑面积")
+    private Double area;
+
+    @Schema(description = "建筑高度")
+    private Double high;
+
+    @Schema(description = "地上层数(层)")
+    private Integer floorsGround;
+
+    @Schema(description = "吊层数(层)")
+    private Integer hangingLayers;
+
+    @Schema(description = "地下层数(层)")
+    private Integer floorsUnderground;
+
+    @Schema(description = "总层数(层)")
+    private Integer floorsTotal;
+
+    @Schema(description = "避难层个数(个)")
+    private Integer hideFloorNum;
+
+    @Schema(description = "避难层位置(层)")
+    private String hideFloorPosition;
+
+    @Schema(description = "专用配电房(间)")
+    private Integer powerDistributionLocation;
+
+    @Schema(description = "专用配电房位置(层)")
+    private String approvalStatus;
+
+    @Schema(description = "消防控制室(间)")
+    private Integer fireControlNum;
+
+    @Schema(description = "消防控制室位置(层)")
+    private String fireControlPosition;
+
+    @Schema(description = "消防水泵房(间)")
+    private Integer firePumpNum;
+
+    @Schema(description = "消防水泵房位置(层)")
+    private String firePumpLocation;
+
+    @Schema(description = "发电机房(间)")
+    private Integer generatornnNum;
+
+    @Schema(description = "发电机房位置(层)")
+    private String generatorPosition;
+
+    @Schema(description = "其他重要设备(间)")
+    private Integer otherImportantNum;
+
+    @Schema(description = "其他重要设备房位置(层)")
+    private String otherImportantPosition;
+
+    @Schema(description = "建造年代(年)")
+    private String buildEra;
+
+    @Schema(description = "消防安全责任人")
+    private String dutyerName;
+
+    @Schema(description = "责任人电话")
+    private String dutyerPhone;
+
+    @Schema(description = "经度")
+    private String lon;
+
+    @Schema(description = "纬度")
+    private String lat;
+
+    @Schema(description = "审核拒绝原因")
+    private String rejectReason;
+
+    @Schema(description = "备注信息")
+    private String remark;
+
+    @Schema(description = "火灾危险性")
+    private Integer fireDanger;
+
+    @Schema(description = "耐火等级")
+    private Integer fireProof;
+
+    @Schema(description = "结构类型")
+    private String structureType;
+    @Schema(description = "毗邻建筑物情况")
+    private Integer abutBuild;
+    @Schema(description = "安全出口位置")
+    private String emergencyExit;
+
+    @Schema(description = "安全出口形式")
+    private Integer emergencyExitType;
+    @Schema(description = "安全出口数量")
+    private Integer emergencyExitNum;
+
+    @Schema(description = "消防电梯数量")
+    private Integer fireLifeNum;
+    @Schema(description = "日常工作时间人数")
+    private Integer workerNum;
+    @Schema(description = "最大容纳人数")
+    private Integer accommodateMaxNum;
+    @Schema(description = "储存物名称")
+    private String storageName;
+
+    @Schema(description = "消防电梯容纳总重量")
+    private Integer lifeAccommodateNum;
+    @Schema(description = "储存物性质")
+    private String storageNameNature;
+    @Schema(description = "储存物形态")
+    private Integer storageType;
+    @Schema(description = "储存容积")
+    private Integer storageVolume;
+    @Schema(description = "储存物数量")
+    private Integer storageNum;
+    @Schema(description = "主要产品")
+    private String products;
+    @Schema(description = "主要原料")
+    private String rawMaterial;
+    @Schema(description = "建筑立面图id")
+    private String facadeId;
+    @Schema(description = "消防设施平面布置图Id")
+    private String planeId;
+    @Schema(description = "建筑平面图id")
+    private String buildPlaneId;
+    @Schema(description = "消防安全责任人照片id")
+    private String fireDutyerimg;
+    @Schema(description = "消防安全管理人照片id")
+    private String fireManagerImg;
+    @Schema(description = "产权人")
+    private String propertyer;
+    @Schema(description = "产权人联系方式")
+    private String propertyerPhone;
+    @Schema(description = "产权人身份证号码")
+    private String propertyerCard;
+
+    @Schema(description = "产权人照片id")
+    private String propertyerPhotoId;
+    @Schema(description = "消防安全责任人身份证号码")
+    private String fireDutyerCard;
+    @Schema(description = "消防安全管理人身份证号码")
+    private String fireManagerCard;
+    @Schema(description = "建筑编码")
+    private String buildCode;
+    @Schema(description = "创建人")
+    private String fileCreateBy;
+    @Schema(description = "最后修改人")
+    private String fileUpdateBy;
+    @Schema(description = "最后修改时间")
+    private Long fileUpdateTime;
+
+
+}

+ 109 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ArchitectureEntity.java

@@ -0,0 +1,109 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.pro.cqfire.enums.ApprovalStatusType;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import javax.validation.constraints.NotBlank;
+import java.sql.JDBCType;
+
+/**
+ * @author gyl
+ * @version 1.0
+ * @time 2022/2/22 11:17
+ */
+@Setter
+@Getter
+@Table(name = "dev_architecture")
+public class ArchitectureEntity extends GenericEntity<String> implements RecordCreationEntity {
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "建筑名称")
+    private String name;
+
+    @Column
+    @Schema(description = "建筑类别")
+    private String type;
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "详细地址")
+    private String address;
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "管理单位id")
+    private String managementUnitId;
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "管理单位")
+    private String managementUnitName;
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "消防组织单位id")
+    private String organizationId;
+
+    @NotBlank
+    @Column(nullable = false)
+    @Schema(description = "消防组织单位")
+    private String organizationName;
+
+    @Column
+    @Schema(description = "消防安全管理人")
+    private String managerName;
+
+    @Column
+    @Schema(description = "管理人电话")
+    private String managerPhone;
+
+    @Column
+    @Schema(description = "创建时间")
+    private Long fileCreateTime;
+
+    @Column
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "审核状态")
+    private ApprovalStatusType approvalStatus;
+
+    @Column
+    @JsonCodec
+    @ColumnType(jdbcType = JDBCType.CLOB, javaType = String.class)
+    @Schema(description = "建筑详细信息")
+    private ArchitectureDetailsInfo detailsInfo;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+}

+ 68 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/CategoryTestItemEntity.java

@@ -0,0 +1,68 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.jetlinks.pro.cqfire.enums.TestItemType;
+import org.jetlinks.pro.cqfire.subscriber.DeviceTestRule;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import javax.validation.constraints.NotBlank;
+
+
+@Getter
+@Setter
+@Table(name = "dev_category_test_item")
+public class CategoryTestItemEntity {
+
+    @Schema(description = "分类id")
+    @Column(nullable = false)
+    private String categoryId;
+
+    @Schema(description = "分类key")
+    @Column(nullable = false)
+    private String categoryKey;
+
+    @Schema(description = "分类名称")
+    @Column(nullable = false)
+    @NotBlank
+    private String categoryName;
+
+    @Column(nullable = false)
+    @Schema(description = "名称")
+    private String name;
+
+    @Column
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "类型")
+    private TestItemType type;
+
+    @Column
+    @Schema(description = "标识")
+    private String key;
+
+    @Column
+    @Schema(description = "字段(类型为“事件”时,选择事件参数值)")
+    private String parameter;
+
+    @Column
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "操作符")
+    private DeviceTestRule.Operator operator;
+
+    @Column
+    @Schema(description = "条件")
+    private String condition;
+
+    @Column(nullable = false)
+    @Schema(description = "是否为必测项")
+    @DefaultValue("true")
+    private Boolean enableTest;
+
+}

+ 155 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/CorpEntity.java

@@ -0,0 +1,155 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.authorization.Dimension;
+import org.hswebframework.web.authorization.simple.SimpleDimension;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.pro.cqfire.authorize.CorpDimensionType;
+import org.jetlinks.pro.cqfire.enums.CorpProcessStatus;
+import org.jetlinks.pro.cqfire.enums.CorpStatus;
+import org.jetlinks.pro.cqfire.enums.CorpType;
+
+import javax.persistence.Column;
+import javax.persistence.Index;
+import javax.persistence.Table;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+@Table(name = "dev_corp", indexes = {
+    @Index(name = "license_dev_corp_unique", columnList = "license", unique = true),
+    @Index(name = "user_dev_corp_unique", columnList = "creator_id", unique = true)
+}
+)
+@EnableEntityEvent
+public class CorpEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Schema(description = "企业名称")
+    @Column(nullable = false)
+    private String name;
+
+    @Column(length = 32, nullable = false)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "企业性质")
+    @DefaultValue("factory")
+    private CorpType type;
+
+    @Schema(description = "企业简称")
+    @Column(nullable = false)
+    private String shortName;
+
+    @Schema(description = "三证合一注册号")
+    @Column(nullable = false)
+    private String license;
+
+    @Schema(description = "企业编码")
+    @Column
+    private Short code;
+
+    @Schema(description = "联系人")
+    @Column(nullable = false)
+    private String contacts;
+
+    @Schema(description = "企业地址")
+    @Column(nullable = false)
+    private String address;
+
+    @Schema(description = "驻渝-办公室地址")
+    @Column
+    private String cqAddress;
+
+    @Schema(description = "营业执照图片地址")
+    @Column(nullable = false)
+    private String licenseImage;
+
+    @Column(length = 32, nullable = false)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "审核状态", accessMode = Schema.AccessMode.READ_ONLY)
+    @DefaultValue("processing")
+    private CorpProcessStatus processStatus;
+
+    @Column(length = 32, nullable = false)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "状态", accessMode = Schema.AccessMode.READ_ONLY)
+    @DefaultValue("enabled")
+    private CorpStatus status;
+
+    @Comment("说明")
+    @Column(name = "describe")
+    @Schema(description = "说明")
+    private String describe;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+
+    public Dimension toDimension() {
+        Map<String, Object> options = new HashMap<>();
+
+        options.put("license", getLicense());
+        options.put("processStatus", getProcessStatus());
+        options.put("shortName", getShortName());
+        options.put("describe", getDescribe());
+        options.put("status", getStatus());
+        return SimpleDimension.of(getId(), getName(), CorpDimensionType.INSTANCE, options);
+    }
+}

+ 103 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/NoticeEntity.java

@@ -0,0 +1,103 @@
+package org.jetlinks.pro.cqfire.entity;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.pro.cqfire.enums.NoticeState;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+import java.util.List;
+
+/**
+ * @author kyl
+ */
+@Setter
+@Getter
+@Table(name = "dev_notice")
+public class NoticeEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Column(nullable = false)
+    @Schema(description = "公告标题")
+    private String title;
+
+    @Column
+    @Schema(description = "公告来源")
+    private String source;
+
+    @Column
+    @Schema(description = "公告内容")
+    @ColumnType(jdbcType = JDBCType.LONGVARCHAR)
+    private String content;
+
+    @Column
+    @Schema(description = "附件地址")
+    @JsonCodec
+    @ColumnType(jdbcType = JDBCType.CLOB, javaType = String.class)
+    private List<FileEntity> address;
+
+    @Column(length = 32, nullable = false)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "状态")
+    @DefaultValue("unpublished")
+    private NoticeState state;
+
+    @Column(updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(description = "创建时间(只读)")
+    private Long createTime;
+
+    @Column(updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Getter
+    @Setter
+    static class FileEntity {
+        private String name;
+        private String url;
+    }
+}

+ 113 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ReportEntity.java

@@ -0,0 +1,113 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.pro.cqfire.enums.ReportTestState;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Table(name = "dev_report")
+@Getter
+@Setter
+@EnableEntityEvent
+public class ReportEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Column(nullable = false)
+    @Schema(description = "报告名称")
+    private String name;
+
+    @Column
+    @Schema(description = "厂商ID")
+    private String corpId;
+
+    @Column
+    @Schema(description = "测试设备ID")
+    private String deviceId;
+
+    @Column
+    @Schema(description = "设备名称")
+    private String deviceName;
+
+    @Column
+    @Schema(description = "测试状态")
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @DefaultValue("toBeTest")
+    private ReportTestState state;
+
+    @Column
+    @Schema(description = "测试通过时间")
+    private Long passTime;
+
+    @Column
+    @Schema(description = "测试开始时间")
+    private Long testTime;
+
+    @Column
+    @DefaultValue("false")
+    @Schema(description = "是否公示报告详情")
+    private Boolean enable;
+
+    @Column
+    @Schema(description = "已通过测试项数量")
+    private Integer passedItems;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+}

+ 81 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/ServerAddressEntity.java

@@ -0,0 +1,81 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.crud.generator.Generators;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+
+/**
+ * @author kyl
+ */
+@Table(name = "dev_server_address")
+@Getter
+@Setter
+public class ServerAddressEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+    @Column
+    @Schema(description = "TCP服务器IP")
+    private String tcpId;
+
+    @Column
+    @Schema(description = "TCP端口")
+    private String tcpPort;
+
+    @Column
+    @Schema(description = "UDP端口")
+    private String udpId;
+
+    @Column
+    @Schema(description = "UDP端口")
+    private String udpPort;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+}

+ 94 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/SystemConfEntity.java

@@ -0,0 +1,94 @@
+package org.jetlinks.pro.cqfire.entity;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.crud.generator.Generators;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+import java.util.List;
+
+
+/**
+ * @author kyl
+ */
+@Getter
+@Setter
+@Table(name = "dev_system_config")
+public class SystemConfEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Column
+    @Schema(description = "登录背景图片")
+    private String loginUrl;
+
+    @Column
+    @Schema(description = "轮播图图片地址")
+    @JsonCodec
+    @ColumnType(jdbcType = JDBCType.CLOB, javaType = String.class)
+    private List<String> slideShowUrl;
+
+    @Column
+    @Schema(description = "备案号")
+    private String number;
+
+    @Column
+    @Schema(description = "版权信息")
+    private String copyRight;
+
+    @Column
+    @Schema(description = "其他信息")
+    private String information;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+}

+ 122 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/TestItemEntity.java

@@ -0,0 +1,122 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.hswebframework.web.api.crud.entity.RecordModifierEntity;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.hswebframework.web.crud.generator.Generators;
+import org.jetlinks.pro.cqfire.enums.TestItemType;
+import org.jetlinks.pro.cqfire.enums.TestState;
+import org.jetlinks.pro.cqfire.subscriber.DeviceTestRule;
+
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+
+/**
+ * @author kyl
+ */
+@Getter
+@Setter
+@Table(name = "dev_test_item")
+@EnableEntityEvent
+public class TestItemEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Column(nullable = false)
+    @Schema(description = "名称")
+    private String name;
+
+    @Column
+    @Schema(description = "设备ID")
+    private String deviceId;
+
+    @Column(nullable = false)
+    @Schema(description = "测试设备ID")
+    private String deviceTestId;
+
+    @Column
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "类型")
+    private TestItemType type;
+
+    @Column
+    @Schema(description = "标识")
+    private String key;
+
+    @Column
+    @Schema(description = "字段(类型为“事件”时,选择事件参数值)")
+    private String parameter;
+
+    @Column
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "操作符")
+    private DeviceTestRule.Operator operator;
+
+    @Column
+    @Schema(description = "条件")
+    private String condition;
+
+    @Column(nullable = false)
+    @Schema(description = "是否为必测项")
+    @DefaultValue("true")
+    private Boolean enableTest;
+
+    @Column(length = 32, nullable = false)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "测试项状态")
+    @DefaultValue("noTest")
+    private TestState state;
+
+    @Column
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "修改时间"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long modifyTime;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人ID"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierId;
+
+    @Column(length = 64)
+    @Schema(
+        description = "修改人名称"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String modifierName;
+
+    @Column(name = "creator_id", updatable = false)
+    @Schema(
+        description = "创建者ID(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorId;
+
+    @Column(name = "creator_name", updatable = false)
+    @Schema(
+        description = "创建者名称(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private String creatorName;
+
+    @Column(name = "create_time", updatable = false)
+    @DefaultValue(generator = Generators.CURRENT_TIME)
+    @Schema(
+        description = "创建时间(只读)"
+        , accessMode = Schema.AccessMode.READ_ONLY
+    )
+    private Long createTime;
+}

+ 128 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/entity/UnitEntity.java

@@ -0,0 +1,128 @@
+package org.jetlinks.pro.cqfire.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.jetlinks.pro.cqfire.enums.Level;
+import org.jetlinks.pro.cqfire.enums.SocialUnitProcessStatus;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+
+@Getter
+@Setter
+@Table(name = "dev_social_unit")
+public class UnitEntity extends GenericEntity<String>{
+
+    @Schema(description = "单位项目名称")
+    @Column(name = "social_unit_name")
+    private String name;
+
+    @Schema(description = "详细地址")
+    @Column
+    private String address;
+
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "重点单位等级(1:一级;2:二级;3:三级)")
+    @Column
+    private Level grade;
+
+    @Schema(description = "是否高危单位(1:是;0:否)")
+    @Column
+    private Boolean danger;
+
+    @Schema(description = "企业名称")
+    @Column
+    private String enterpriseName;
+
+    @Schema(description = "统一社会编码")
+    @Column
+    private String enterpriseCode;
+
+    @Schema(description = "所在街道")
+    @Column
+    private String street;
+
+    @Schema(description = "楼层")
+    @Column
+    private String floor;
+
+    @Schema(description = "经营面积")
+    @Column
+    private Double businessArea;
+
+    @Schema(description = "固定资产")
+    @Column
+    private Double assets;
+
+    @Schema(description = "单位性质")
+    @Column
+    private String nature;
+
+    @Schema(description = "所属行业")
+    @Column
+    private String industry;
+
+    @Schema(description = "是否具有消控室(1:是;0:否)")
+    @Column
+    private Boolean fireControl;
+
+    @Schema(description = "区域Id")
+    @Column
+    private String regionId;
+
+    @Schema(description = "街道id")
+    @Column
+    private String streetId;
+
+    @Schema(description = "联系人姓名")
+    @Column
+    private String contactsName;
+
+    @Schema(description = "联系人电话")
+    @Column
+    private String contactsPhone;
+
+    @Schema(description = "联网情况(1:是;0:否)")
+    @Column
+    private Boolean netWorking;
+
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @Schema(description = "审核状态:1.待审核2.已驳回3.已审核")
+    @Column
+    private SocialUnitProcessStatus auditStatus;
+
+    @Schema(description = "审核拒绝原因")
+    @Column
+    private String rejectReason;
+
+    @Schema(description = "备注信息")
+    @Column
+    private String remark;
+
+    @Schema(description = "创建人")
+    @Column
+    private String fileCreateBy;
+
+    @Schema(description = "创建时间")
+    @Column
+    private Long fileCreateTime;
+
+    @Schema(description = "最后修改人")
+    @Column
+    private String fileUpdateBy;
+
+    @Schema(description = "最后修改时间")
+    @Column
+    private Long fileUpdateTime;
+
+    @Schema(description = "注册时间")
+    @Column
+    private Long registerTime;
+
+}

+ 27 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/ApprovalStatusType.java

@@ -0,0 +1,27 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * 审核状态;1.待审核2.已驳回;3.已审核
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum ApprovalStatusType implements I18nEnumDict<String> {
+    pendingReview("待审核", "1"),
+    rejected("已驳回", "2"),
+    audited("已审核", "3"),
+    ;
+    private final String text;
+    private final String sign;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return this.getSign();
+    }
+}

+ 28 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpProcessStatus.java

@@ -0,0 +1,28 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum CorpProcessStatus implements I18nEnumDict<String> {
+    init("初始化"),
+    passed("通过"),
+    failed("未通过"),
+    processing("审核中");
+    private final String text;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+}

+ 26 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpStatus.java

@@ -0,0 +1,26 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum CorpStatus implements I18nEnumDict<String> {
+    enabled("启用"),
+    disabled("禁用");
+    private final String text;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+}

+ 26 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/CorpType.java

@@ -0,0 +1,26 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum CorpType implements I18nEnumDict<String> {
+    factory("设备厂商"),
+    integrator("集成商");
+    private final String text;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+}

+ 28 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/Level.java

@@ -0,0 +1,28 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+import org.springframework.data.relational.core.sql.In;
+
+@AllArgsConstructor
+@Getter
+@Generated
+public enum Level implements I18nEnumDict<Integer> {
+    Level1("一级",1),
+    Level2("二级",2),
+    Level3("三级",3);
+
+
+    private final String text;
+    private final int sign;
+
+    @Override
+    @Generated
+    public Integer getValue() {
+        return getSign();
+    }
+
+
+}

+ 26 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/NoticeState.java

@@ -0,0 +1,26 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+
+/**
+ * @author kyl
+ */
+
+@AllArgsConstructor
+@Getter
+@Generated
+public enum NoticeState implements I18nEnumDict<String> {
+    published("已发布"),
+    unpublished("未发布");
+    private final String text;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+}

+ 33 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/Operator.java

@@ -0,0 +1,33 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.reactor.ql.utils.CastUtils;
+
+@AllArgsConstructor
+@Getter
+public enum Operator {
+    eq("="),
+    not("!="),
+    gt(">"),
+    lt("<"),
+    gte(">="),
+    lte("<="),
+    like("like") {
+        @Override
+        public Object convert(String value) {
+            if (value.contains("%")) {
+                return super.convert(value);
+            }
+            return "%" + value + "%";
+        }
+    };
+    private final String symbol;
+
+    public Object convert(String value) {
+        if (org.hswebframework.utils.StringUtils.isNumber(value)) {
+            return CastUtils.castNumber(value);
+        }
+        return value;
+    }
+}

+ 27 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/ReportTestState.java

@@ -0,0 +1,27 @@
+package org.jetlinks.pro.cqfire.enums;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author kyl
+ */
+
+@Getter
+@Generated
+@AllArgsConstructor
+public enum ReportTestState implements I18nEnumDict<String> {
+    test("测试中"),
+    toBeTest("待测试"),
+    qualified("合格");
+
+    private final String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+}

+ 27 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/SocialUnitProcessStatus.java

@@ -0,0 +1,27 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author df
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Generated
+public enum SocialUnitProcessStatus implements I18nEnumDict<String> {
+    passed("待审核"),
+    failed("已驳回"),
+    processing("已审核");
+    private final String text;
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+}

+ 92 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/TestItemType.java

@@ -0,0 +1,92 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+import org.jetlinks.pro.cqfire.web.item.TestItemRequest;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@AllArgsConstructor
+@Getter
+@Generated
+public enum TestItemType implements I18nEnumDict<String> {
+    properties("属性") {
+        @Override
+        boolean judgeItem(JSONObject metadata, TestItemRequest request) {
+            List<Object> collect = metadata
+                .getJSONArray("properties")
+                .stream()
+                .filter(j -> request
+                    .getKey()
+                    .equals(JSONObject.parseObject(j.toString()).getString("id")))
+                .collect(Collectors.toList());
+            return 0 != collect.size();
+        }
+    },
+    event("事件") {
+        @Override
+        boolean judgeItem(JSONObject metadata, TestItemRequest request) {
+            List<Object> collect = metadata
+                .getJSONArray("events")
+                .stream()
+                .filter(j -> request
+                    .getKey()
+                    .equals(JSONObject.parseObject(j.toString()).getString("id")))
+                .filter(j -> {
+                    if (StringUtils.hasText(request.getParameter())) {
+                        List<Object> collect1 = JSONObject.parseObject(j.toString())
+                                                          .getJSONObject("valueType")
+                                                          .getJSONArray("properties")
+                                                          .stream()
+                                                          .filter(e -> request
+                                                              .getParameter()
+                                                              .equals(JSONObject
+                                                                          .parseObject(e.toString())
+                                                                          .getString("id")))
+                                                          .collect(Collectors.toList());
+                        return 0 != collect1.size();
+                    }
+                    return true;
+                })
+                .collect(Collectors.toList());
+            return 0 != collect.size();
+        }
+    },
+    online("上线") {
+        @Override
+        boolean judgeItem(JSONObject metadata, TestItemRequest request) {
+            return true;
+        }
+    },
+    offline("离线") {
+        @Override
+        boolean judgeItem(JSONObject metadata, TestItemRequest request) {
+            return true;
+        }
+    };
+    private final String text;
+
+    abstract boolean judgeItem(JSONObject metadata, TestItemRequest request);
+
+    @Override
+    @Generated
+    public String getValue() {
+        return name();
+    }
+
+    public static boolean judge(String metadata, TestItemRequest request) {
+        try {
+            return request.getType().judgeItem(JSONObject.parseObject(metadata), request);
+        } catch (Exception e) {
+            throw new UnsupportedOperationException("测试项与物模型不匹配");
+        }
+
+    }
+
+
+}

+ 26 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/enums/TestState.java

@@ -0,0 +1,26 @@
+package org.jetlinks.pro.cqfire.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import org.hswebframework.web.dict.I18nEnumDict;
+
+/**
+ * @author kyl
+ */
+
+@Getter
+@Generated
+@AllArgsConstructor
+public enum TestState implements I18nEnumDict<String> {
+    passed("通过"),
+    failed("不通过"),
+    noTest("未测试");
+
+    private final String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+}

+ 23 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/handle/CodeValidatedEvent.java

@@ -0,0 +1,23 @@
+package org.jetlinks.pro.cqfire.handle;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.event.DefaultAsyncEvent;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+@Builder
+public class CodeValidatedEvent extends DefaultAsyncEvent {
+
+    private String token;
+
+    private String username;
+
+    private String telephone;
+}

+ 23 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/handle/VerifyCodeEvent.java

@@ -0,0 +1,23 @@
+package org.jetlinks.pro.cqfire.handle;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.event.DefaultAsyncEvent;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Getter
+@Setter
+public class VerifyCodeEvent extends DefaultAsyncEvent {
+
+    private String token;
+
+    private String code;
+
+    private String receiver;
+}

+ 102 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/OrgInitService.java

@@ -0,0 +1,102 @@
+package org.jetlinks.pro.cqfire.service;
+
+import com.alibaba.fastjson.JSON;
+import org.hswebframework.utils.PinyinUtils;
+import org.hswebframework.utils.RandomUtil;
+import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
+import org.hswebframework.web.id.IDGenerator;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.jetlinks.pro.auth.entity.OrganizationEntity;
+import org.jetlinks.pro.auth.service.OrganizationService;
+import org.jetlinks.pro.authorize.OrgDimensionType;
+import org.jetlinks.pro.openapi.manager.entity.OpenApiClientEntity;
+import org.jetlinks.pro.openapi.manager.service.LocalOpenApiClientService;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Component
+public class OrgInitService implements CommandLineRunner {
+
+    private final OrganizationService organizationService;
+    private final LocalOpenApiClientService clientService;
+    private final DefaultDimensionUserService dimensionUserService;
+
+    public OrgInitService(OrganizationService organizationService,
+                          LocalOpenApiClientService clientService,
+                          DefaultDimensionUserService dimensionUserService) {
+        this.organizationService = organizationService;
+        this.clientService = clientService;
+        this.dimensionUserService = dimensionUserService;
+    }
+
+    @Override
+    public void run(String... args) throws Exception {
+        organizationService
+            .createQuery()
+            .fetchOne()
+            .switchIfEmpty(initDefaultData().then(Mono.empty()))
+            .subscribe();
+    }
+
+    private Mono<Void> initDefaultData() {
+        return Mono
+            .fromCallable(() -> {
+                ClassPathResource resource = new ClassPathResource("cqfire-org.json");
+
+                try (InputStream stream = resource.getInputStream()) {
+                    String json = StreamUtils.copyToString(stream, StandardCharsets.UTF_8);
+
+                    List<OrganizationEntity> all = JSON.parseArray(json, OrganizationEntity.class);
+                    return all;
+                }
+            })
+            .flatMap(all -> organizationService
+                .save(Flux.fromIterable(TreeSupportEntity.list2tree(all, OrganizationEntity::setChildren)))
+                .then(createOpenApiClient(Flux.fromIterable(all)))
+            )
+            ;
+    }
+
+    private Mono<Void> createOpenApiClient(Flux<OrganizationEntity> all) {
+        return all
+            .flatMap(org -> {
+                OpenApiClientEntity clientEntity = new OpenApiClientEntity();
+                clientEntity.setClientName(org.getName());
+                clientEntity.setEnableOAuth2(false);
+                clientEntity.setSecureKey(RandomUtil.randomChar(16));
+                clientEntity.setId(IDGenerator.SNOW_FLAKE_HEX.generate());
+                clientEntity.setSignature("MD5");
+                clientEntity.setUsername(PinyinUtils.toPinYinHeadChar(org.getName()));
+                clientEntity.setPassword("xf123456");
+                return clientService
+                    .insert(clientEntity)
+                    .flatMap(r -> {
+                        DimensionUserEntity dimensionUserEntity = new DimensionUserEntity();
+                        dimensionUserEntity.setUserName(org.getName());
+                        dimensionUserEntity.setUserId(clientEntity.getUserId());
+                        dimensionUserEntity.setDimensionId(org.getId());
+                        dimensionUserEntity.setDimensionName(org.getName());
+                        dimensionUserEntity.setDimensionTypeId(OrgDimensionType.org.getId());
+                        dimensionUserEntity.generateId();
+                        return dimensionUserService.save(Mono.just(dimensionUserEntity));
+                    })
+                    ;
+            })
+            .then();
+    }
+
+}

+ 97 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/architecture/ArchitectureExcelInfo.java

@@ -0,0 +1,97 @@
+package org.jetlinks.pro.cqfire.service.architecture;
+
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.reactor.excel.CellDataType;
+import org.hswebframework.reactor.excel.ExcelHeader;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.pro.cqfire.entity.ArchitectureEntity;
+import org.springframework.util.ObjectUtils;
+import reactor.core.publisher.Flux;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * @author gyl
+ * @time 2021/7/16 13:45
+ */
+
+@Getter
+@Setter
+@Slf4j
+@NoArgsConstructor
+public class ArchitectureExcelInfo {
+
+    @Schema(description = "建筑名称")
+    private String name;
+
+    @Schema(description = "建筑类别")
+    private String type;
+
+    @Schema(description = "详细地址")
+    private String address;
+
+    @Schema(description = "管理单位")
+    private String managementUnitName;
+
+    @Schema(description = "消防安全管理人")
+    private String managerName;
+
+    @Schema(description = "管理人电话")
+    private String managerPhone;
+
+    @Schema(description = "创建时间")
+    private String registrationTimeString;
+
+    @Schema(description = "审核状态")
+    private String approvalStatusSting;
+
+    @Schema(description = "建筑详细信息")
+    private String detailsInfoString;
+
+    public static ArchitectureExcelInfo of(ArchitectureEntity architectureEntity) {
+        ArchitectureExcelInfo info = FastBeanCopier.copy(architectureEntity, new ArchitectureExcelInfo());
+        if (!ObjectUtils.isEmpty(architectureEntity.getFileCreateTime())) {
+            info.setRegistrationTimeString(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(architectureEntity.getFileCreateTime())));
+        }
+        if (!ObjectUtils.isEmpty(architectureEntity.getApprovalStatus())) {
+            info.setApprovalStatusSting(architectureEntity.getApprovalStatus().getText());
+        }
+        if (!ObjectUtils.isEmpty(architectureEntity.getDetailsInfo())) {
+            info.setDetailsInfoString(JSONObject.toJSONString(architectureEntity.getDetailsInfo()));
+        }
+        return info;
+    }
+
+
+    public static List<ExcelHeader> getHeaderMapping() {
+        return new ArrayList<>(Arrays.asList(
+            new ExcelHeader("name", "建筑名称", CellDataType.STRING),
+            new ExcelHeader("type", "建筑类别", CellDataType.STRING),
+            new ExcelHeader("address", "详细地址", CellDataType.STRING),
+            new ExcelHeader("managementUnitName", "管理单位", CellDataType.STRING),
+            new ExcelHeader("managerName", "消防安全管理人", CellDataType.STRING),
+            new ExcelHeader("managerPhone", "管理人电话", CellDataType.STRING),
+            new ExcelHeader("registrationTimeString", "创建时间", CellDataType.STRING),
+            new ExcelHeader("approvalStatusSting", "审核状态", CellDataType.STRING),
+            new ExcelHeader("detailsInfoString", "建筑详细信息", CellDataType.STRING)
+        ));
+    }
+
+    public static Flux<ArchitectureExcelInfo> getContentMapping(Flux<ArchitectureEntity> entitys) {
+        return entitys.flatMap(e -> Flux.just(ArchitectureExcelInfo.of(e)))
+                      .doOnError(err -> log.error(err.getMessage(), err));
+    }
+
+    public Map<String, Object> toMap() {
+        return FastBeanCopier.copy(this, new HashMap<>(9));
+    }
+
+
+}
+

+ 275 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/architecture/ArchitectureService.java

@@ -0,0 +1,275 @@
+package org.jetlinks.pro.cqfire.service.architecture;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.MapUtils;
+import org.hswebframework.web.crud.events.EntityCreatedEvent;
+import org.hswebframework.web.crud.events.EntitySavedEvent;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.pro.assets.AssetBindManager;
+import org.jetlinks.pro.assets.AssetBindRequest;
+import org.jetlinks.pro.assets.AssetUnbindRequest;
+import org.jetlinks.pro.authorize.OrgDimensionType;
+import org.jetlinks.pro.cqfire.entity.ArchitectureEntity;
+import org.jetlinks.pro.cqfire.web.unit.SocialUnitController;
+import org.jetlinks.pro.device.entity.DeviceInstanceEntity;
+import org.jetlinks.pro.device.service.LocalDeviceInstanceService;
+import org.jetlinks.pro.device.tenant.DeviceAssetType;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.GroupedFlux;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class ArchitectureService extends GenericReactiveCrudService<ArchitectureEntity, String> {
+
+    private final LocalDeviceInstanceService deviceInstanceService;
+    private final AssetBindManager assetBindManager;
+
+    public final static String PREFIX = "m";
+
+
+    public Mono<Void> bindAssetBindFromManagement(List<String> managementUnitIds) {
+        return getDeviceIdsFromManagement(managementUnitIds)
+            .filter(map -> !MapUtils.isEmpty(map))
+            .flatMapMany(map -> Flux
+                .fromIterable(map.keySet())
+                .flatMap(managementUnitId -> {
+                    List<String> deviceIds = map.get(managementUnitId);
+                    return bindDevice(PREFIX + managementUnitId, deviceIds);
+                }))
+            .then();
+
+
+    }
+
+    public Mono<Map<String, List<String>>> getDeviceIdsFromManagement(List<String> managementUnitIds) {
+        return this.createQuery()
+                   .select(ArchitectureEntity::getId, ArchitectureEntity::getManagementUnitId)
+                   .in(ArchitectureEntity::getManagementUnitId, managementUnitIds)
+                   .fetch()
+                   .collectMap(ArchitectureEntity::getId, ArchitectureEntity::getManagementUnitId)
+                   .filter(map -> !MapUtils.isEmpty(map))
+                   .flatMap(archMap -> {
+                       return deviceInstanceService
+                           .createQuery()
+                           .select(DeviceInstanceEntity::getId, DeviceInstanceEntity::getArchitectureId)
+                           .in(DeviceInstanceEntity::getArchitectureId, archMap.keySet())
+                           .fetch()
+                           .collectList()
+                           .flatMap(device -> {
+                               Map<String, DeviceInstanceEntity> deviceMap = device
+                                   .stream()
+                                   .collect(Collectors.toMap(DeviceInstanceEntity::getId, Function.identity()));
+                               return deviceInstanceService
+                                   .createQuery()
+                                   .select(DeviceInstanceEntity::getId)
+                                   .in(DeviceInstanceEntity::getParentId, deviceMap.keySet())
+                                   .fetch()
+                                   .mergeWith(Flux.fromIterable(device))
+                                   .filter(d -> StringUtils.hasText(d.getArchitectureId()) || StringUtils.hasText(d.getParentId()))
+                                   .collect(Collectors.groupingBy(d -> {
+                                                                      String architectureId = d.getArchitectureId();
+                                                                      if (architectureId == null) {
+                                                                          String parentId = d.getParentId();
+                                                                          architectureId = deviceMap
+                                                                              .get(parentId)
+                                                                              .getArchitectureId();
+                                                                      }
+                                                                      return archMap.get(architectureId);
+                                                                  },
+                                                                  Collectors.mapping(DeviceInstanceEntity::getId, Collectors.toList())));
+                           })
+                           ;
+                   });
+    }
+
+    @Getter
+    @AllArgsConstructor
+    static class DeviceBindManagementData {
+        String managementUnitId;
+        String deviceIds;
+    }
+
+
+    /**
+     * 新增设备绑定资产
+     */
+    @EventListener
+    public void handleEvent(EntityCreatedEvent<DeviceInstanceEntity> event) {
+        List<DeviceInstanceEntity> entitys = event.getEntity();
+        event.async(
+            bind(Flux.fromIterable(entitys))
+        );
+
+    }
+
+    /**
+     * 新增设备绑定资产 自注册也是这边
+     */
+    @EventListener
+    public void handleEvent(EntitySavedEvent<DeviceInstanceEntity> event) {
+        List<DeviceInstanceEntity> entitys = event.getEntity();
+        event.async(
+            bind(Flux.fromIterable(entitys))
+        );
+
+    }
+
+    private Flux<Void> bind(Flux<DeviceInstanceEntity> entitys) {
+        return entitys
+            .flatMap(entity -> {
+                if (StringUtils.hasText(entity.getParentId())) {
+                    return doBindComponents(entity)
+                        .onErrorResume(err -> {
+                            log.warn("绑定失败{},{}", entity.getId(), err);
+                            return Mono.empty();
+                        });
+                } else if (StringUtils.hasText(entity.getArchitectureId())) {
+                    return this
+                        .findById(entity.getArchitectureId())
+                        .flatMap(architectureEntity -> doBind(entity.getId(), architectureEntity))
+                        .onErrorResume(err -> {
+                            log.warn("绑定失败{},{}", entity.getId(), err);
+                            return Mono.empty();
+                        });
+                }
+                return Mono.empty();
+            });
+    }
+
+    /**
+     * 查询子设备一起绑定支队及社会端
+     */
+    private Mono<Void> doBind(String id, ArchitectureEntity architectureEntity) {
+        return Mono
+            .just(id)
+            .mergeWith(deviceInstanceService
+                           .createQuery()
+                           .where(DeviceInstanceEntity::getParentId, id)
+                           .fetch()
+                           .map(DeviceInstanceEntity::getId))
+            .collectList()
+            .flatMap(list -> {
+//                list.add(id);
+                String orgId = architectureEntity.getOrganizationId();
+                String managerId = SocialUnitController.PREFIX + architectureEntity.getManagementUnitId();
+                return assetBindManager
+                    .unbindAllAssets(DeviceAssetType.device.getId(), list)
+                    .then(bindDevice(orgId, list))
+                    .then(bindDevice(managerId, list));
+            });
+    }
+
+    /**
+     * 直接绑定支队及社会端
+     */
+    private Mono<Void> doBind(List<String> ids, ArchitectureEntity architectureEntity) {
+        if (!ids.isEmpty()) {
+            String orgId = architectureEntity.getOrganizationId();
+            String managerId = SocialUnitController.PREFIX + architectureEntity.getManagementUnitId();
+            return assetBindManager
+                .unbindAllAssets(DeviceAssetType.device.getId(), ids)
+                .flatMap(i -> {
+                    if (StringUtils.hasText(orgId) && StringUtils.hasText(architectureEntity.getManagementUnitId())) {
+                        return bindDevice(orgId, ids)
+                            .then(bindDevice(managerId, ids));
+                    } else if (StringUtils.hasText(orgId)) {
+                        return bindDevice(orgId, ids);
+                    } else if (StringUtils.hasText(architectureEntity.getManagementUnitId())) {
+                        return bindDevice(managerId, ids);
+                    }
+                    return Mono.empty();
+                });
+        }
+        return Mono.empty();
+    }
+
+    private Mono<Void> doBindComponents(DeviceInstanceEntity entity) {
+        return deviceInstanceService
+            .createQuery()
+            .where(DeviceInstanceEntity::getId, entity.getParentId())
+            .fetchOne()
+            .map(DeviceInstanceEntity::getArchitectureId)
+            .flatMap(architectureId -> deviceInstanceService
+                .createUpdate()
+                .set(DeviceInstanceEntity::getArchitectureId, architectureId)
+                .where(DeviceInstanceEntity::getId, entity.getId())
+                .execute()
+                .thenReturn(architectureId))
+            .flatMap(architectureId -> this
+                .findById(architectureId)
+                .flatMap(architectureEntity -> doBind(Collections.singletonList(entity.getId()), architectureEntity)));
+    }
+
+    public Flux<GroupedFlux<String, String>> getDeviceIds(List<String> architectureIds) {
+        return deviceInstanceService
+            .createQuery()
+            .in(DeviceInstanceEntity::getArchitectureId, architectureIds)
+            .fetch()
+            .groupBy(DeviceInstanceEntity::getArchitectureId, DeviceInstanceEntity::getId);
+    }
+
+
+    public Mono<List<String>> getDeviceId(String architectureId) {
+        return deviceInstanceService
+            .createQuery()
+            .where(DeviceInstanceEntity::getArchitectureId, architectureId)
+            .fetch()
+            .map(DeviceInstanceEntity::getId)
+            .collect(Collectors.toList());
+    }
+
+
+    public Mono<Void> unBindDevice(String oldOrgId, List<String> deviceIds) {
+        return assetBindManager
+            .unbindAssets(Mono.just(AssetUnbindRequest
+                                        .builder()
+                                        .assetIdList(deviceIds)
+                                        .assetType(DeviceAssetType.device.getId())
+                                        .targetId(oldOrgId)
+                                        .targetType(OrgDimensionType.org.getId())
+                                        .build()));
+    }
+
+    public Mono<Void> bindDevice(String orgId, List<String> deviceIds) {
+        return assetBindManager
+            .bindAssets(Mono.just(AssetBindRequest
+                                      .builder()
+                                      .targetType(OrgDimensionType.org.getId())
+                                      .targetId(orgId)
+                                      .assetType(DeviceAssetType.device.getId())
+                                      .assetIdList(deviceIds)
+                                      .build()));
+    }
+
+    public Flux<Void> reBind() {
+        return this.createQuery()
+                   .fetch()
+                   .buffer(200)
+                   .flatMap(architectureEntities -> {
+                       Map<String, ArchitectureEntity> architectureEntityMap = architectureEntities
+                           .stream()
+                           .collect(Collectors.toMap(ArchitectureEntity::getId, a -> a));
+                       return this
+                           .getDeviceIds(new ArrayList<>(architectureEntityMap.keySet()))
+                           .flatMap(g -> g
+                               .collectList()
+                               .flatMap(deviceIds -> doBind(deviceIds, architectureEntityMap.get(g.key()))));
+                   });
+    }
+
+}

+ 254 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/corp/CorpService.java

@@ -0,0 +1,254 @@
+package org.jetlinks.pro.cqfire.service.corp;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.crud.events.EntityBeforeCreateEvent;
+import org.hswebframework.web.crud.events.EntityModifyEvent;
+import org.hswebframework.web.crud.events.EntitySavedEvent;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.exception.NotFoundException;
+import org.hswebframework.web.id.IDGenerator;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.api.entity.UserEntity;
+import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveUserService;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.pro.auth.entity.UserDetail;
+import org.jetlinks.pro.auth.entity.UserDetailEntity;
+import org.jetlinks.pro.auth.service.UserDetailService;
+import org.jetlinks.pro.cqfire.entity.CorpEntity;
+import org.jetlinks.pro.cqfire.enums.CorpProcessStatus;
+import org.jetlinks.pro.cqfire.enums.CorpStatus;
+import org.jetlinks.pro.cqfire.service.user.UserService;
+import org.jetlinks.pro.cqfire.web.corp.ChangeProcessStatusRequest;
+import org.jetlinks.pro.cqfire.web.corp.ChangeStatusRequest;
+import org.jetlinks.pro.cqfire.web.user.CorpUserDetail;
+import org.reactivestreams.Publisher;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class CorpService extends GenericReactiveCrudService<CorpEntity, String> {
+    private final UserDetailService userDetailService;
+    private final DefaultReactiveUserService defaultReactiveUserService;
+    private final DefaultDimensionUserService dimensionUserService;
+    private final ApplicationEventPublisher eventPublisher;
+
+    private final static String dimensionId = "default-corp-role";
+
+    @EventListener
+    public void handleEntitySavedEvent(EntitySavedEvent<CorpEntity> event) {
+        event
+            .async(
+                saveUserName(event.getEntity())
+            );
+    }
+
+    @EventListener
+    public void handleEntitySavedEvent(EntityModifyEvent<CorpEntity> event) {
+        event.async(saveUserName(event.getAfter()));
+    }
+
+    @EventListener
+    public void handleEntitySavedEvent(EntityBeforeCreateEvent<CorpEntity> event) {
+        event
+            .async(
+                saveUserName(event.getEntity())
+            );
+    }
+
+    public Mono<Integer> saveUserName(List<CorpEntity> corpEntityList) {
+
+        return saveAllUserName(corpEntityList
+                                   .stream()
+                                   .map(corpEntity -> {
+                                       UserEntity entity = new UserEntity();
+                                       entity.setId(corpEntity.getCreatorId());
+                                       entity.setName(corpEntity.getContacts());
+                                       return entity;
+                                   })
+                                   .collect(Collectors.toList()))
+            ;
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Boolean> changeProcessStatus(ChangeProcessStatusRequest request) {
+        ValidatorUtils.tryValidate(request);
+        return findCorp(request.getCorpId())
+            .then(findById(request.getCorpId())
+                      .flatMap(corpEntity -> bindCorpRoleById(corpEntity.getCreatorId(), corpEntity.getContacts())
+                          .doOnNext(i -> eventPublisher
+                              .publishEvent(ClearUserAuthorizationCacheEvent.of(corpEntity.getCreatorId())))))
+            .then(this.createUpdate()
+                      .where()
+                      .and(CorpEntity::getId, request.getCorpId())
+                      .set(CorpEntity::getProcessStatus, request.getStatus())
+                      .set(CorpEntity::getDescribe,
+                           StringUtils.isEmpty(request.getDescribe()) ? "" : request.getDescribe())
+                      .execute()
+                      .map(res -> res > 0));
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Boolean> changeStatus(ChangeStatusRequest request) {
+        ValidatorUtils.tryValidate(request);
+        return findCorp(request.getCorpId())
+            .then(this.createUpdate()
+                      .where()
+                      .and(CorpEntity::getId, request.getCorpId())
+                      .set(CorpEntity::getStatus, request.getStatus())
+                      .execute()
+                      .map(res -> res > 0)
+                      .flatMap(b -> findById(request.getCorpId())
+                          .flatMap(corpEntity -> changeState(Mono.just(corpEntity.getCreatorId()),
+                                                             getUserState(request.getStatus()))
+                          )
+                               .thenReturn(b)
+                      )
+            );
+    }
+
+    public Mono<CorpUserDetail> getCorpUserDetailByUserId(String userId) {
+        return this.createQuery()
+                   .where()
+                   .and(CorpEntity::getCreatorId, userId)
+                   .fetchOne()
+                   .map(corpEntity -> {
+                       CorpUserDetail detail = new CorpUserDetail();
+                       detail.setCorpInfo(corpEntity);
+                       return detail;
+                   })
+                   .flatMap(detail -> getUserDetailById(userId)
+                       .map(detail::setUserDetail));
+    }
+
+    public Mono<PagerResult<CorpUserDetail>> queryCorpUserDetails(QueryParamEntity param) {
+        return this.queryPager(param)
+                   .filter(e -> !e.getData().isEmpty())
+                   .flatMap(result -> Flux
+                       .fromIterable(result.getData())
+                       .flatMap(corpEntity -> {
+                           CorpUserDetail detail = new CorpUserDetail();
+                           detail.setCorpInfo(corpEntity);
+                           return Mono.just(detail);
+                       })
+                       .flatMap(detail -> getUserDetailById(detail.getCreatorId())
+                           .map(detail::setUserDetail))
+                       .collectList()
+                       .map(list -> PagerResult.of(result.getTotal(), list, param)))
+                   .defaultIfEmpty(PagerResult.empty());
+    }
+
+    private Mono<Integer> bindDimension(List<CorpEntity> corpEntityList) {
+        return Flux.fromIterable(corpEntityList)
+                   .flatMap(corpEntity -> bindCorpRoleById(corpEntity.getCreatorId(), corpEntity.getContacts())
+                   )
+                   .reduce(Math::addExact)
+            ;
+    }
+
+    private byte getUserState(CorpStatus status) {
+        return CorpStatus.disabled.equals(status) ? (byte) 0 : (byte) 1;
+    }
+
+
+    private Mono<Void> findCorp(String id) {
+        return findById(id)
+            .switchIfEmpty(Mono.error(new NotFoundException("企业不存在!")))
+            .then();
+    }
+
+
+    private Mono<Boolean> findDimensionByIdAndUserId(String dimensionId, String userId) {
+        return dimensionUserService
+            .createQuery()
+            .where()
+            .and(DimensionUserEntity::getDimensionId, dimensionId)
+            .and(DimensionUserEntity::getUserId, userId)
+            .count()
+            .map(res -> res > 0);
+    }
+
+    public Mono<Integer> bindCorpRoleById(String id, String name) {
+        return findDimensionByIdAndUserId(dimensionId, id)
+            .filter(aBoolean -> !aBoolean)
+            .flatMap(b -> {
+                DimensionUserEntity dimensionUserEntity = new DimensionUserEntity();
+                dimensionUserEntity.setUserId(id);
+                dimensionUserEntity.setUserName(name);
+                dimensionUserEntity.setDimensionId(dimensionId);
+                dimensionUserEntity.setDimensionTypeId(DefaultDimensionType.role.getId());
+                dimensionUserEntity.setDimensionName("企业默认角色");
+                dimensionUserEntity.generateId();
+                return dimensionUserService
+                    .save(Mono.just(dimensionUserEntity))
+                    .map(SaveResult::getTotal);
+            });
+    }
+
+    public Mono<CorpEntity> commit(CorpEntity entity) {
+        return Mono.defer(() -> {
+            entity.setProcessStatus(CorpProcessStatus.processing);
+            if (StringUtils.isEmpty(entity.getId())) {
+                String id = IDGenerator.SNOW_FLAKE_STRING.generate();
+                entity.setId(id);
+                return this
+                    .insert(entity)
+                    .then(findById(id));
+            }
+            return this.findById(entity.getId())
+                       .switchIfEmpty(Mono.error(new NotFoundException("企业不存在!")))
+                       .flatMap(corpEntity -> this
+                           .save(entity)
+                           .doOnNext(r -> eventPublisher
+                               .publishEvent(ClearUserAuthorizationCacheEvent
+                                                 .of(entity.getCreatorId())))
+                           .then(Mono.just(corpEntity)));
+        });
+    }
+
+
+    public Mono<Integer> saveAllUserName(List<UserEntity> userEntityList) {
+
+        return Flux.fromIterable(userEntityList)
+                   .flatMap(userEntity -> {
+                       UserDetailEntity entity = new UserDetailEntity();
+                       entity.setId(userEntity.getId());
+                       entity.setName(userEntity.getName());
+                       return userDetailService.updateById(userEntity.getId(), entity)
+                                               .then(defaultReactiveUserService.updateById(userEntity.getId(), userEntity));
+                   })
+                   .reduce(Math::addExact)
+            ;
+
+    }
+
+    public Mono<Integer> changeState(Publisher<String> userId, byte state) {
+        return defaultReactiveUserService.changeState(userId, state);
+    }
+
+    public Mono<UserDetail> getUserDetailById(String id) {
+        return userDetailService.findUserDetail(id);
+    }
+}

+ 60 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/item/CategoryTestItemService.java

@@ -0,0 +1,60 @@
+package org.jetlinks.pro.cqfire.service.item;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.pro.cqfire.entity.CategoryTestItemEntity;
+import org.jetlinks.pro.cqfire.web.item.TestItemRequest;
+import org.jetlinks.pro.device.entity.DeviceCategoryEntity;
+import org.jetlinks.pro.device.service.DeviceCategoryService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+@AllArgsConstructor
+public class CategoryTestItemService extends GenericReactiveCrudService<CategoryTestItemEntity, String> {
+    private final DeviceCategoryService categoryService;
+
+
+    public Flux<CategoryTestItemEntity> getCategoryItems(String categoryId) {
+        return categoryService
+            .findById(categoryId)
+            .flatMapMany(category -> this
+                .createQuery()
+                .where(CategoryTestItemEntity::getCategoryId, category.getId())
+                .and(CategoryTestItemEntity::getCategoryKey, category.getKey())
+                .and(CategoryTestItemEntity::getCategoryName, category.getName())
+                .selectExcludes("categoryId","categoryKey","categoryName")
+                .fetch()
+            );
+    }
+
+
+    public Flux<CategoryTestItemEntity> updateByCategory(String categoryId, Flux<TestItemRequest> request) {
+        return categoryService
+            .findById(categoryId)
+            .flatMapMany(category -> this.addItems(category, request));
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Flux<CategoryTestItemEntity> addItems(DeviceCategoryEntity category, Flux<TestItemRequest> request) {
+        return this
+            .createDelete()
+            .where(CategoryTestItemEntity::getCategoryId, category.getId())
+            .and(CategoryTestItemEntity::getCategoryKey, category.getKey())
+            .and(CategoryTestItemEntity::getCategoryName, category.getName())
+            .execute()
+            .flatMapMany(i -> request
+                .flatMap(res -> {
+                    CategoryTestItemEntity entity = res.toCategoryTestItemEntity(category);
+                    return this.insert(entity).thenReturn(entity);
+                })
+            )
+            .filter(CategoryTestItemEntity::getEnableTest)
+            .switchIfEmpty(Mono.error(new BusinessException("至少有一项测试项为必测项!")));
+    }
+
+}

+ 85 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/item/TestItemService.java

@@ -0,0 +1,85 @@
+package org.jetlinks.pro.cqfire.service.item;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.pro.cqfire.entity.TestItemEntity;
+import org.jetlinks.pro.cqfire.enums.TestItemType;
+import org.jetlinks.pro.cqfire.enums.TestState;
+import org.jetlinks.pro.cqfire.web.item.TestItemRequest;
+import org.jetlinks.pro.cqfire.web.report.TestItemDetail;
+import org.jetlinks.pro.device.entity.DeviceProductEntity;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.jetlinks.pro.device.service.DeviceTestService;
+import org.jetlinks.pro.device.service.LocalDeviceProductService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author kyl
+ */
+@Service
+@AllArgsConstructor
+public class TestItemService extends GenericReactiveCrudService<TestItemEntity, String> {
+    private final DeviceTestService deviceTestService;
+    private final LocalDeviceProductService productService;
+
+    @Transactional
+    public Mono<TestItemEntity> createTestItem(DeviceTestEntity testEntity, TestItemEntity entity) {
+        return Mono.just(entity)
+                   .flatMap(item -> {
+                       item.setDeviceId(testEntity.getSourceAddress());
+                       item.setState(TestState.noTest);
+                       return save(item).thenReturn(item);
+                   });
+    }
+
+    public Flux<TestItemDetail> queryByTestDeviceId(String deviceTestId) {
+        return this.createQuery()
+                   .where()
+                   .and(TestItemEntity::getDeviceTestId, deviceTestId)
+                   .fetch()
+                   .flatMap(item -> {
+                       TestItemDetail detail = new TestItemDetail();
+                       detail.setId(item.getId());
+                       detail.setName(item.getName());
+                       detail.setState(item.getState());
+                       detail.setEnableTest(item.getEnableTest());
+                       return Mono.just(detail);
+                   });
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Flux<TestItemEntity> addTestItems(String deviceId, Authentication auth, Flux<TestItemRequest> request) {
+        return createDelete()
+            .where(TestItemEntity::getDeviceTestId, deviceId)
+            .execute()
+            .flatMap(i -> deviceTestService
+                .findById(deviceId)
+                .flatMap(testEntity -> productService
+                    .findById(testEntity.getProductId())
+                    .map(DeviceProductEntity::getMetadata)
+                    .zipWith(Mono.just(testEntity))
+                ))
+            .flatMapMany(metadataAndTestEntity -> request
+                .flatMap(res -> {
+                    boolean judge = TestItemType.judge(metadataAndTestEntity.getT1(), res);
+                    if (judge) {
+                        TestItemEntity entity = res.toTestItemEntity();
+                        entity.setDeviceTestId(deviceId);
+                        entity.setCreatorId(auth.getUser().getId());
+                        entity.setCreatorName(auth.getUser().getName());
+                        return createTestItem(metadataAndTestEntity.getT2(), entity);
+                    } else {
+                        throw new UnsupportedOperationException("测试项与物模型不匹配");
+                    }
+                })
+            )
+            .filter(TestItemEntity::getEnableTest)
+            .switchIfEmpty(Mono.error(new BusinessException("至少有一项测试项为必测项!")));
+    }
+}

+ 12 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/notice/NoticeService.java

@@ -0,0 +1,12 @@
+package org.jetlinks.pro.cqfire.service.notice;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.pro.cqfire.entity.NoticeEntity;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class NoticeService extends GenericReactiveCrudService<NoticeEntity, String> {
+
+}

+ 520 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/report/ReportPdfInfo.java

@@ -0,0 +1,520 @@
+package org.jetlinks.pro.cqfire.service.report;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fop.configuration.DefaultConfigurationBuilder;
+import org.hswebframework.printer.*;
+import org.hswebframework.printer.layer.LineLayer;
+import org.hswebframework.printer.layer.RectLayer;
+import org.hswebframework.printer.layer.TextLayer;
+import org.jetlinks.pro.cqfire.entity.CorpEntity;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.cqfire.enums.TestState;
+import org.jetlinks.pro.cqfire.web.report.TestItemDetail;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.springframework.util.ResourceUtils;
+
+import java.awt.*;
+import java.io.*;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.List;
+
+/**
+ * @author kyl
+ */
+@Getter
+@Setter
+@Slf4j
+public class ReportPdfInfo {
+
+    @Schema(description = "报告名称")
+    private String name;
+
+    @Schema(description = "厂商ID")
+    private String corpId;
+
+    @Schema(description = "厂商名称")
+    private String corpName;
+
+    @Schema(description = "企业简称")
+    private String shortName;
+
+    @Schema(description = "产品ID")
+    private String productId;
+
+    @Schema(description = "产品名称")
+    private String productName;
+
+    @Schema(description = "设备ID")
+    private String deviceId;
+
+    @Schema(description = "设备名称")
+    private String deviceName;
+
+    @Schema(description = "设备类型")
+    private String productType;
+
+    @Schema(description = "设备型号")
+    private String model;
+
+    @Schema(description = "3C证书编号")
+    private String license;
+
+    @Schema(description = "状态")
+    private String state;
+
+    @Schema(description = "测试开始时间")
+    private String testTime;
+
+    @Schema(description = "驻渝-办公室地址")
+    private String cqAddress;
+
+    @Schema(description = "源地址")
+    private String sourceAddress;
+
+    @Schema(description = "设备测试项信息")
+    private List<TestItemDetail> items = new ArrayList<>();
+
+    private static String installFontPath = "classpath:fop-configuration.xml";
+
+    public static ReportPdfInfo getExportDataContent(DeviceTestEntity testEntity, CorpEntity corpEntity, ReportEntity reportEntity, List<TestItemDetail> itemEntity) {
+        ReportPdfInfo info = new ReportPdfInfo();
+        info.setCorpName(testEntity.getCorpName());
+        info.setCqAddress(corpEntity.getCqAddress());
+        info.setShortName(corpEntity.getShortName());
+        info.setDeviceName(testEntity.getName());
+        info.setProductName(testEntity.getProductName());
+        info.setProductId(testEntity.getProductId());
+        info.setName(reportEntity.getName());
+        info.setModel(testEntity.getModel());
+        info.setProductType(testEntity.getClassifiedName());
+        info.setLicense(testEntity.getLicense());
+        info.setState(reportEntity.getState().getText());
+        info.setSourceAddress(testEntity.getSourceAddress());
+        info.setTestTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(reportEntity.getTestTime())));
+        info.setItems(itemEntity);
+        return info;
+    }
+
+    public static byte[] testDownloadReport(ReportPdfInfo info) throws Exception {
+        Pager pager1 = new Pager();
+        List<Layer> layers1 = new ArrayList<>();
+
+        Pager pager2 = new Pager();
+        List<Layer> layers2 = new ArrayList<>();
+
+        List<List<Layer>> lists = new ArrayList<>();
+        lists.add(layers1);
+        lists.add(layers2);
+
+        List<Pager> pageList = new ArrayList<>();
+        pageList.add(pager1);
+        pageList.add(pager2);
+
+        int size = info.getItems().size();
+        int currentPage = size / 20 + 2;
+
+        //页面和布局
+        for (int i = 1; i < currentPage; i++) {
+            if (i > 1) {
+                Pager nowPager = new Pager();
+                List<Layer> nowLayers = new ArrayList<>();
+                lists.add(nowLayers);
+                pageList.add(nowPager);
+            }
+        }
+
+        //水印
+        for (int i = 0; i < 6; i++) {
+            for (int j = 0; j < 6; j++) {
+                for (List<Layer> layers : lists) {
+                    layers.add(convertWatermark(i, j));
+                    layers.add(convertWatermark2(i, j));
+                }
+            }
+        }
+
+        //第一页数据
+        {
+            Font font = new Font("方正小标宋_GBK", Font.PLAIN, 22);
+            TextLayer layer = new TextLayer();
+            layer.setX(0);
+            layer.setY(100);
+            layer.setColor(Color.black);
+            layer.setWidth(600);
+            layer.setHeight(100);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("重庆消防物联网接入管理平台\n 测试报告");
+            layer.setAlign(TextLayer.Align.center);
+            layer.setFont(font);
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setX(90);
+            layer.setY(200);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(200);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("        基于重庆消防物联网接入管理平台,按照重庆市消防\n救援总队发布的《重庆消防物联网信息传输设备网络通信\n协议》,对" + info.getCorpName() + "申请的设备进行了通信协议和数据传输测试。\n        申请测试设备基础信息及测试结果如下。");
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        //内容
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(350);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("测试单位:" + info.getCorpName());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(380);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("产品名称:" + info.getProductName());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(440);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("产品编号:" + info.productId);
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(470);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("设备类型:" + info.productType);
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(500);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("设备型号:" + info.getModel());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(530);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("设备源地址:" + info.getSourceAddress());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(560);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("3C证书编号:" + info.getLicense());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(590);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(40);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("申请时间:" + info.getTestTime());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(620);
+            layer.setX(120);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("测试状态:" + info.getState());
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers1.add(layer);
+        }
+
+        {
+            TextLayer layer = new TextLayer();
+            layer.setY(70);
+            layer.setX(100);
+            layer.setColor(Color.BLACK);
+            layer.setWidth(400);
+            layer.setHeight(30);
+            layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+            layer.setText("测试结果如下:");
+            layer.setAlign(TextLayer.Align.left);
+            layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+            layers2.add(layer);
+        }
+
+        if (!info.getItems().isEmpty()) {
+            //表格 ---测试项矩形框
+            for (int i = 1; i < currentPage; i++) {
+                if (i == (currentPage - 1)) {
+                    drawTable(lists.get(i), size - (i - 1) * 20);
+                    break;
+                }
+                drawTable(lists.get(i), 20);
+            }
+
+            //测试项标题框和信息添加
+            {
+                RectLayer layer = new RectLayer();
+                layer.setY(100);
+                layer.setX(100);
+                layer.setWidth(400);
+                layer.setHeight(30);
+                layer.setColor(Color.BLACK);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers2.add(layer);
+            }
+            {
+                LineLayer layer = new LineLayer();
+                layer.setY(100);
+                layer.setX(400);
+                layer.setEndY(130);
+                layer.setEndX(400);
+                layer.setColor(Color.BLACK);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers2.add(layer);
+            }
+            {
+                TextLayer layer = new TextLayer();
+                layer.setY(105);
+                layer.setX(100);
+                layer.setColor(Color.BLACK);
+                layer.setWidth(300);
+                layer.setHeight(30);
+                layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+                layer.setText("申请测试项");
+                layer.setAlign(TextLayer.Align.center);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers2.add(layer);
+            }
+            {
+                TextLayer layer = new TextLayer();
+                layer.setY(105);
+                layer.setX(400);
+                layer.setColor(Color.BLACK);
+                layer.setWidth(100);
+                layer.setHeight(30);
+                layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+                layer.setText("测试结果");
+                layer.setAlign(TextLayer.Align.center);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers2.add(layer);
+            }
+
+            //测试项信息
+            int count = 0;
+            for (int i = 1; i < currentPage; i++) {
+                if (i == currentPage - 1) {
+                    writeTestItems(info, lists.get(i), count, size, size - (i - 1) * 20);
+                    break;
+                }
+                count = writeTestItems(info, lists.get(i), count, size, 20);
+            }
+
+            //*:必测项
+            {
+                TextLayer layer = new TextLayer();
+                layer.setY(130 + (size - (currentPage - 2) * 20) * 30);
+                layer.setX(400);
+                layer.setColor(Color.BLACK);
+                layer.setWidth(100);
+                layer.setHeight(30);
+                layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+                layer.setText("(*:必测项)");
+                layer.setAlign(TextLayer.Align.left);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                lists.get(currentPage - 1).add(layer);
+            }
+        }
+
+        for (int i = 0; i < currentPage; i++) {
+            pageList.get(i).setLayers(lists.get(i));
+            pageList.get(i).setOrientation(0);
+        }
+
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrinterUtils
+            .printToPdf(pageList
+                , new PixelPaper(72, Paper.A4)
+                , output
+                , new DefaultConfigurationBuilder().build(new FileInputStream(ResourceUtils.getFile(installFontPath))));
+
+        return output.toByteArray();
+    }
+
+    public static String getDate() {
+        Calendar cal = Calendar.getInstance();
+        int month = cal.get(Calendar.MONTH) + 1;
+        int year = cal.get(Calendar.YEAR);
+        int day = cal.get(Calendar.DATE);
+        return year + "年" + month + "月" + day + "日";
+    }
+
+    public static TextLayer convertWatermark(int i, int j) {
+        TextLayer layer = new TextLayer();
+        layer.setX(250 * i);
+        layer.setY(250 * j);
+        layer.setColor(new Color(241, 239, 239));
+        layer.setAngdeg(45);
+        layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+        layer.setText("重庆消防物联网接入管理平台");
+        layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+        layer.setAlign(TextLayer.Align.center);
+        return layer;
+    }
+
+    public static TextLayer convertWatermark2(int i, int j) {
+        TextLayer layer = new TextLayer();
+        layer.setX(255 * i);
+        layer.setY(255 * j);
+        layer.setColor(new Color(241, 239, 239));
+        layer.setAngdeg(45);
+        layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+        layer.setText(getDate());
+        layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+        layer.setAlign(TextLayer.Align.center);
+        return layer;
+    }
+
+    public static void drawTable(List<Layer> layers, int size) {
+        for (int i = 130; i < (130 + size * 30); i += 30) {
+            {
+                RectLayer layer = new RectLayer();
+                layer.setY(i);
+                layer.setX(100);
+                layer.setWidth(400);
+                layer.setHeight(30);
+                layer.setColor(Color.BLACK);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers.add(layer);
+            }
+            {
+                LineLayer layer = new LineLayer();
+                layer.setY(i);
+                layer.setX(400);
+                layer.setEndY(i + 30);
+                layer.setEndX(400);
+                layer.setColor(Color.BLACK);
+                layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                layers.add(layer);
+            }
+        }
+    }
+
+    public static int writeTestItems(ReportPdfInfo info, List<Layer> layers, int count, int size, int length) {
+        for (int i = 130; i < (130 + length * 30); i += 30) {
+            if (count < size) {
+                //测试项信息
+                {
+                    TextLayer layer = new TextLayer();
+                    layer.setY(i + 5);
+                    layer.setX(100);
+                    layer.setColor(Color.black);
+                    layer.setWidth(300);
+                    layer.setHeight(30);
+                    layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+                    if (info.getItems().get(count).getEnableTest().equals(true)) {
+                        layer.setText("* " + info.getItems()
+                                                 .get(count)
+                                                 .getName());
+                    } else {
+                        layer.setText(info.getItems()
+                                          .get(count)
+                                          .getName());
+                    }
+                    layer.setAlign(TextLayer.Align.center);
+                    layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                    layers.add(layer);
+                }
+                {
+                    TextLayer layer = new TextLayer();
+                    layer.setY(i + 5);
+                    layer.setX(400);
+                    layer.setColor(Color.BLACK);
+                    layer.setWidth(100);
+                    layer.setHeight(30);
+                    layer.setVerticalAlign(TextLayer.VerticalAlign.top);
+                    if (info.getItems().get(count).getEnableTest().equals(false) &&
+                        info.getItems().get(count).getState().equals(TestState.noTest)) {
+                        layer.setText("-");
+                    } else {
+                        layer.setText(info.getItems()
+                                          .get(count)
+                                          .getState()
+                                          .getText());
+                    }
+                    layer.setAlign(TextLayer.Align.center);
+                    layer.setFont(new Font("方正仿宋_GBK", Font.PLAIN, 16));
+                    count++;
+                    layers.add(layer);
+                }
+            }
+        }
+        return count;
+    }
+}

+ 519 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/report/ReportService.java

@@ -0,0 +1,519 @@
+package org.jetlinks.pro.cqfire.service.report;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.core.param.TermType;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.crud.events.EntityCreatedEvent;
+import org.hswebframework.web.crud.events.EntityDeletedEvent;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.id.IDGenerator;
+import org.jetlinks.core.event.EventBus;
+import org.jetlinks.core.event.Subscription;
+import org.jetlinks.pro.cqfire.entity.CorpEntity;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.cqfire.entity.TestItemEntity;
+import org.jetlinks.pro.cqfire.enums.ReportTestState;
+import org.jetlinks.pro.cqfire.enums.TestState;
+import org.jetlinks.pro.cqfire.service.corp.CorpService;
+import org.jetlinks.pro.cqfire.service.item.TestItemService;
+import org.jetlinks.pro.cqfire.subscriber.DeviceTestProvider;
+import org.jetlinks.pro.cqfire.subscriber.DeviceTestRule;
+import org.jetlinks.pro.cqfire.subscriber.DeviceTestTaskExecutor;
+import org.jetlinks.pro.cqfire.web.report.ReportResponse;
+import org.jetlinks.pro.cqfire.web.report.TestItemDetail;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.jetlinks.pro.device.service.DeviceTestService;
+import org.jetlinks.pro.gateway.annotation.Subscribe;
+import org.reactivestreams.Publisher;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author kyl
+ */
+@Service
+@Slf4j
+public class ReportService extends GenericReactiveCrudService<ReportEntity, String> implements CommandLineRunner {
+    private final CorpService corpService;
+    private final DeviceTestService deviceTestService;
+    private final TestItemService testItemService;
+
+    private final EventBus eventBus;
+    private final DeviceTestProvider deviceTestProvider;
+
+    private final Map<String, Disposable> subscribers = new ConcurrentHashMap<>();
+
+    public ReportService(CorpService corpService,
+                         DeviceTestService deviceTestService,
+                         TestItemService testItemService, EventBus eventBus,
+                         DeviceTestProvider deviceTestProvider) {
+        this.corpService = corpService;
+        this.deviceTestService = deviceTestService;
+        this.testItemService = testItemService;
+        this.eventBus = eventBus;
+        this.deviceTestProvider = deviceTestProvider;
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<ReportEntity> createReport(ReportEntity reportEntity) {
+        String id = IDGenerator.SNOW_FLAKE_STRING.generate();
+        return testItemService
+            .queryByTestDeviceId(reportEntity.getDeviceId())
+            .switchIfEmpty(Mono.error(new BusinessException("先配置测试项!")))
+            .then(createQuery()
+                      .where(ReportEntity::getDeviceId, reportEntity.getDeviceId())
+                      .fetch()
+                      .count()
+                      .filter(i -> i < 1)
+                      .switchIfEmpty(Mono.error(new BusinessException("已创建该设备测试报告!")))
+                      .flatMap(res -> deviceTestService
+                          .findById(reportEntity.getDeviceId())
+                          .flatMap(entity -> {
+                              reportEntity.setId(id);
+                              reportEntity.setName(entity.getName() + "的报告");
+                              reportEntity.setState(ReportTestState.test);
+                              reportEntity.setCorpId(entity.getCorpId());
+                              reportEntity.setEnable(false);
+                              reportEntity.setDeviceName(entity.getName());
+                              reportEntity.setPassedItems(0);
+                              reportEntity.setCreateTime(System.currentTimeMillis());
+                              return insert(reportEntity).thenReturn(reportEntity);
+                          })));
+    }
+
+    public Mono<Integer> changeReportState(String id, Boolean enable) {
+        return createUpdate()
+            .set(ReportEntity::getEnable, enable)
+            .where(ReportEntity::getId, id)
+            .execute();
+    }
+
+    public Mono<TestItemEntity> testReportComplete(String reportId, TestItemEntity testItemEntity) {
+        return Mono.just(testItemEntity)
+                   .flatMap(entity -> testItemService
+                       .updateById(entity.getId(), entity)
+                       .map(res -> res > 0)
+                       .filter(item -> entity.getState().equals(TestState.passed))
+                       .switchIfEmpty(Mono.empty())
+                       .flatMap(a -> findById(reportId)
+                           .flatMap(reportEntity -> createUpdate()
+                               .where(ReportEntity::getDeviceId, entity.getDeviceTestId())
+                               .set(ReportEntity::getPassedItems, reportEntity.getPassedItems() + 1)
+                               .execute())
+                       ))
+                   .then(testItemService
+                             .createQuery()
+                             .where(TestItemEntity::getDeviceTestId, testItemEntity.getDeviceTestId())
+                             .fetch()
+                             .filter(item -> !item.getState().equals(TestState.passed)
+                                 && item.getEnableTest().equals(true))
+                             .count()
+                             .filter(i -> i == 0)
+                             .flatMap(res -> createUpdate()
+                                 .where(ReportEntity::getId, reportId)
+                                 .set(ReportEntity::getPassTime, System.currentTimeMillis())
+                                 .set(ReportEntity::getState, ReportTestState.qualified)
+                                 .execute()))
+                   .thenReturn(testItemEntity);
+    }
+
+    public Mono<ReportResponse> queryReportById(String id) {
+        ReportResponse response = new ReportResponse();
+        return createQuery()
+            .where(ReportEntity::getId, id)
+            .fetchOne()
+            .map(ReportResponse::of)
+            .flatMap(report -> deviceTestService
+                .findById(report.getDeviceId())
+                .map(deviceInstanceEntity -> response.fromDeviceTestEntity(report, deviceInstanceEntity)))
+            .flatMap(res -> corpService
+                .findById(res.getCorpId())
+                .flatMap(corpEntity -> {
+                    res.setCqAddress(corpEntity.getCqAddress());
+                    res.setShortName(corpEntity.getShortName());
+                    return addItems(res);
+                }));
+    }
+
+    public Mono<PagerResult<ReportResponse>> queryAllDetails(QueryParamEntity param) {
+        return this.queryPager(param)
+                   .filter(e -> !e.getData().isEmpty())
+                   .flatMap(result -> this.convertReportAllDetail(result.getData())
+                                          .collectList()
+                                          .map(this::convertReportByCorpId)
+                                          .flatMapMany(res -> res.sort(Comparator
+                                                                           .comparing(ReportResponse::getTestTime)
+                                                                           .reversed()))
+                                          .collectList()
+                                          .map(list -> PagerResult.of(result.getTotal(), list, param)))
+                   .defaultIfEmpty(PagerResult.of(0, Collections.emptyList(), param));
+    }
+
+    public Mono<Integer> deleteDeviceAndReport(Publisher<String> idPublisher) {
+        return Flux.from(idPublisher)
+                   .collectList()
+                   .flatMap(ids -> createDelete()
+                       .where()
+                       .in(ReportEntity::getDeviceId, ids)
+                       .execute()
+                       .then(deviceTestService
+                                 .createUpdate()
+                                 .where()
+                                 .in(DeviceTestEntity::getId, ids)
+                                 .set(DeviceTestEntity::getDeleteState, true)
+                                 .execute()));
+    }
+
+    public Mono<Integer> deleteOneDeviceAndReport(String deviceId) {
+        return createDelete()
+            .where(ReportEntity::getDeviceId, deviceId)
+            .execute()
+            .then(deviceTestService
+                      .createUpdate()
+                      .where(DeviceTestEntity::getId, deviceId)
+                      .set(DeviceTestEntity::getDeleteState, true)
+                      .execute());
+    }
+
+    public Mono<ReportPdfInfo> convertPdfInfo(String deviceId, String corpId) {
+        return Mono
+            .zip(deviceTestService
+                     .findById(deviceId),
+                 corpService
+                     .findById(corpId),
+                 createQuery()
+                     .where(ReportEntity::getDeviceId, deviceId)
+                     .fetchOne(),
+                 testItemService
+                     .queryByTestDeviceId(deviceId)
+                     .sort(Comparator.comparing(TestItemDetail::getId))
+                     .collectList()
+            )
+            .map(tp4 -> ReportPdfInfo
+                .getExportDataContent(tp4.getT1(), tp4.getT2(), tp4.getT3(), tp4.getT4()));
+    }
+
+    public Mono<PagerResult<ReportResponse>> reportPublicDetail(QueryParamEntity paramEntity) {
+        paramEntity.and("enable", TermType.eq, true).and("state", TermType.eq, ReportTestState.qualified);
+
+        return queryPager(paramEntity)
+            .filter(res -> !CollectionUtils.isEmpty(res.getData()))
+            .flatMap(res -> this
+                .convertReportAllDetail(res.getData())
+                .collectList()
+                .map(this::convertReportByCorpId)
+                .flatMapMany(data -> data
+                    .sort(Comparator
+                              .comparing(ReportResponse::getTestTime)
+                              .reversed()))
+                .collectList()
+                .map(list -> PagerResult.of(res.getTotal(), list, paramEntity)))
+            .defaultIfEmpty(PagerResult.empty());
+    }
+
+    public Flux<ReportResponse> convertReportAllDetail(List<ReportEntity> reports) {
+        Map<String, ReportResponse> groupMap = reports
+            .stream()
+            .collect(Collectors.toMap(ReportEntity::getDeviceId, ReportResponse::of));
+
+        return Mono
+            .zip(deviceTestService
+                     .createQuery()
+                     .where()
+                     .in(DeviceTestEntity::getId, groupMap.keySet())
+                     .fetch()
+                     .collectMap(DeviceTestEntity::getId),
+                 testItemService
+                     .createQuery()
+                     .where()
+                     .in(TestItemEntity::getDeviceTestId, groupMap.keySet())
+                     .fetch()
+                     .collectList()
+            )
+            .flatMapMany(tp2 -> {
+                for (Map.Entry<String, ReportResponse> entry : groupMap.entrySet()) {
+                    if (tp2.getT1().get(entry.getKey()) == null) {
+                        continue;
+                    }
+                    //复制属性
+                    entry.getValue().fromDeviceTestEntity(entry.getValue(), tp2.getT1().get(entry.getKey()));
+
+                    List<TestItemDetail> itemDetails = new ArrayList<>();
+                    tp2.getT2()
+                       .forEach(item -> {
+                           if (item.getDeviceTestId().equals(entry.getKey())) {
+                               TestItemDetail detail = new TestItemDetail();
+                               detail.setId(item.getId());
+                               detail.setName(item.getName());
+                               detail.setState(item.getState());
+                               detail.setEnableTest(item.getEnableTest());
+                               itemDetails.add(detail);
+                           }
+                       });
+                    entry.getValue().setProperties(itemDetails);
+                }
+                return Flux.fromIterable(groupMap.values());
+            });
+    }
+
+    public Mono<ReportResponse> queryReportByDeviceId(String deviceId) {
+        return createQuery()
+            .where(ReportEntity::getDeviceId, deviceId)
+            .fetchOne()
+            .map(ReportResponse::of)
+            .flatMap(res -> corpService
+                .findById(res.getCorpId())
+                .flatMap(corpEntity -> {
+                    res.setCqAddress(corpEntity.getCqAddress());
+                    res.setCorpName(corpEntity.getName());
+                    return addItems(res);
+                }));
+    }
+
+    public Flux<ReportResponse> convertReportByCorpId(List<ReportResponse> report) {
+        List<String> corpIds = report.stream().map(ReportResponse::getCorpId).collect(Collectors.toList());
+
+        return corpService
+            .createQuery()
+            .where()
+            .in(CorpEntity::getId, corpIds)
+            .fetch()
+            .collectMap(CorpEntity::getId)
+            .flatMap(entity -> {
+                report.forEach(response -> {
+                    response.setShortName(entity.get(response.getCorpId()).getShortName() == null ? "" : entity
+                        .get(response.getCorpId())
+                        .getShortName());
+                    response.setCqAddress(entity.get(response.getCorpId()).getCqAddress() == null ? "" : entity
+                        .get(response.getCorpId())
+                        .getCqAddress());
+                });
+                return Mono.just(report);
+            })
+            .flatMapMany(Flux::fromIterable);
+    }
+
+    public Mono<ReportResponse> addItems(ReportResponse reportResponse) {
+        return testItemService
+            .queryByTestDeviceId(reportResponse.getDeviceId())
+            .sort(Comparator.comparing(TestItemDetail::getId))
+            .collectList()
+            .flatMap(res -> {
+                reportResponse.setProperties(res);
+                return Mono.just(reportResponse);
+            });
+    }
+
+    public Flux<DeviceTestEntity> findAllTestingDevice() {
+        return this.createQuery()
+                   .where()
+                   .and(ReportEntity::getState, ReportTestState.test)
+                   .fetch()
+                   .map(ReportEntity::getDeviceId)
+                   .collectList()
+                   .flatMapMany(testDeviceIds -> deviceTestService
+                       .createQuery()
+                       .where()
+                       .in(DeviceTestEntity::getId, testDeviceIds)
+                       .fetch()
+                   );
+    }
+
+    private void doReportChange(ReportEntity reportEntity) {
+        eventBus.publish("/report-changed", reportEntity)
+                .retry(3)
+                .doOnNext(l -> handleSubscribe(reportEntity))
+                .subscribe();
+    }
+
+    @EventListener
+    public void handleEvent(EntityCreatedEvent<ReportEntity> event) {
+        event.getEntity().forEach(this::doReportChange);
+    }
+
+    //    @EventListener
+//    public void handleEvent(EntitySavedEvent<ReportEntity> event) {
+//        event.getEntity().forEach(this::doReportChange);
+//    }
+//
+//    @EventListener
+//    public void handleEvent(EntityModifyEvent<ReportEntity> event) {
+//        //event.getAfter().forEach(this::doReportChange);
+//        event.async(
+//            autoControlReportChange(event.getBefore(), event.getAfter())
+//        );
+//    }
+
+    @EventListener
+    public void handleEvent(EntityDeletedEvent<ReportEntity> event) {
+        event.getEntity().forEach(entity -> {
+            entity.setState(ReportTestState.toBeTest);
+            doReportChange(entity);
+        });
+    }
+
+    private Flux<Void> autoControlReportChange(List<ReportEntity> beforeList, List<ReportEntity> afterList) {
+        return Flux.fromIterable(beforeList)
+                   .collect(Collectors.toMap(ReportEntity::getId, Function.identity()))
+                   .flatMapMany(beforeMap -> Flux
+                       .fromIterable(afterList)
+                       .doOnNext(after -> {
+                           ReportEntity before = beforeMap.get(after.getId());
+                           if (ReportTestState.test.equals(after.getState())
+                               && !ReportTestState.test.equals(before.getState())) {
+                               doReportChange(after);
+                           }
+                       })
+                       .then()
+                   );
+    }
+
+    /**
+     * 监听测试设备删除事件,判断改设备是否已开始测试
+     *
+     * @param event
+     */
+    @EventListener
+    public void handleDeviceTestDelete(EntityDeletedEvent<DeviceTestEntity> event) {
+        event.async(getReportState(event.getEntity()));
+    }
+
+    private Mono<Void> getReportState(List<DeviceTestEntity> list) {
+        return createQuery()
+            .where()
+            .in(ReportEntity::getDeviceId, list
+                .stream()
+                .map(DeviceTestEntity::getId)
+                .collect(Collectors.toList())
+            )
+            .fetch()
+            .flatMap(reportEntity -> {
+                if (ReportTestState.test.equals(reportEntity.getState())) {
+                    return Mono.error(new BusinessException("该设备已开始测试"));
+                } else if (ReportTestState.qualified.equals(reportEntity.getState())) {
+                    return Mono.error(new BusinessException("该设备已通过测试"));
+                }
+                return Mono.empty();
+            })
+            .then();
+    }
+
+    @Subscribe(value = "/report-changed", features = Subscription.Feature.broker)
+    public void handleSubscribe(ReportEntity entity) {
+
+
+        //停止测试
+        if (!ReportTestState.test.equals(entity.getState())) {
+            Optional.ofNullable(subscribers.remove(entity.getId()))
+                    .ifPresent(Disposable::dispose);
+            log.debug("unsubscribe:{}({}),{}", entity.getDeviceName(), entity.getDeviceId(), entity.getId());
+            return;
+        }
+        Disposable old = subscribers
+            .put(entity.getId(),
+                 deviceTestService
+                     .createQuery()
+                     .where()
+                     .and(DeviceTestEntity::getId, entity.getDeviceId())
+                     .fetchOne()
+                     .flatMap(deviceTestEntity -> createRule(entity, deviceTestEntity)
+                         .flatMap(this::createTask))
+                     .flatMapMany(DeviceTestTaskExecutor::doSubscribe)
+                     .then()
+                     //.then(eventBus.publish("/device-test/", "")) //推送
+                     .subscribe()
+            );
+        log.debug("subscribe device test:{}{}", entity.getDeviceName(), entity.getId());
+
+        if (null != old) {
+            log.debug("close old subscribe device test:{}{}", entity.getDeviceName(), entity.getId());
+            old.dispose();
+        }
+
+    }
+
+    private Mono<DeviceTestRule> createRule(ReportEntity reportEntity, DeviceTestEntity testEntity) {
+
+        DeviceTestRule rule = new DeviceTestRule();
+        rule.setId(reportEntity.getId());
+        rule.setProductId(testEntity.getProductId());
+        rule.setDeviceId(testEntity.getSourceAddress());
+        rule.setProductName(testEntity.getProductName());
+        rule.setDeviceName(testEntity.getName());
+        return testItemService.createQuery()
+                              .where()
+                              .and(TestItemEntity::getDeviceTestId, testEntity.getId())
+                              //只测试必测项
+                              .and(TestItemEntity::getEnableTest, true)
+                              .not(TestItemEntity::getState, TestState.passed)
+                              .fetch()
+                              .collectList()
+                              .flatMap(items -> {
+                                  List<DeviceTestRule.Trigger> triggers = items
+                                      .stream()
+                                      //.filter(TestItemEntity::getEnableTest)
+                                      .map(testItem -> {
+
+                                          DeviceTestRule.Trigger trigger = new DeviceTestRule.Trigger();
+                                          trigger.setTestItemId(testItem.getId());
+                                          trigger.setModelId(testItem.getKey());
+                                          trigger.setType(DeviceTestRule.MessageType.getType(testItem
+                                                                                                 .getType()
+                                                                                                 .getValue()));
+                                          if (null != createFilter(testItem)) {
+                                              trigger.setFilters(Collections.singletonList(createFilter(testItem)));
+                                          }
+
+                                          return trigger;
+                                      })
+                                      .collect(Collectors.toList());
+                                  rule.setTriggers(triggers);
+                                  return Mono.just(rule);
+                              });
+
+    }
+
+    private DeviceTestRule.ConditionFilter createFilter(TestItemEntity itemEntity) {
+
+        return DeviceTestRule
+            .MessageType
+            .getType(itemEntity.getType().getValue())
+            .createOperator(
+                itemEntity.getKey(),
+                itemEntity.getOperator(),
+                itemEntity.getCondition(),
+                itemEntity.getParameter()
+            );
+    }
+
+    private Mono<DeviceTestTaskExecutor> createTask(DeviceTestRule rule) {
+        return Mono.just(new DeviceTestTaskExecutor(eventBus,
+                                                    this,
+                                                    testItemService,
+                                                    rule));
+    }
+
+    @Override
+    public void run(String... args) {
+        createQuery()
+            .where()
+            .and(ReportEntity::getState, ReportTestState.test)
+            .fetch()
+            .doOnNext(this::doReportChange)
+            .subscribe();
+    }
+}

+ 12 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/server/ServerAddressService.java

@@ -0,0 +1,12 @@
+package org.jetlinks.pro.cqfire.service.server;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.pro.cqfire.entity.ServerAddressEntity;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class ServerAddressService extends GenericReactiveCrudService<ServerAddressEntity, String> {
+
+}

+ 21 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/system/SystemConfService.java

@@ -0,0 +1,21 @@
+package org.jetlinks.pro.cqfire.service.system;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.pro.cqfire.entity.SystemConfEntity;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author kyl
+ */
+@Service
+@AllArgsConstructor
+public class SystemConfService extends GenericReactiveCrudService<SystemConfEntity, String> {
+
+    public Mono<SystemConfEntity> queryConfig() {
+        return createQuery()
+            .where()
+            .fetchOne();
+    }
+}

+ 90 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/term/DeviceModelTerm.java

@@ -0,0 +1,90 @@
+package org.jetlinks.pro.cqfire.service.term;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import org.hswebframework.ezorm.core.param.Term;
+import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
+import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
+import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.*;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder;
+import org.hswebframework.web.api.crud.entity.TermExpressionParser;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+
+@Component
+public class DeviceModelTerm extends AbstractTermFragmentBuilder {
+    public DeviceModelTerm() {
+        super("test-device-detail", "根据设备信息查询测试报告数据");
+    }
+
+    public static List<Term> convertTerms(Object value) {
+        if (value instanceof String) {
+            String strVal = String.valueOf(value);
+            //json字符串
+            if (strVal.startsWith("[")) {
+                value = JSON.parseArray(strVal);
+            } else {
+                //表达式
+                return TermExpressionParser.parse(strVal);
+            }
+        }
+        if (value instanceof List) {
+            return new JSONArray(((List) value)).toJavaList(Term.class);
+        } else {
+            throw new UnsupportedOperationException("unsupported term value:" + value);
+        }
+    }
+
+    @Override
+    public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) {
+        List<Term> terms = convertTerms(term.getValue());
+        PrepareSqlFragments fragments = PrepareSqlFragments.of();
+
+        if (term.getOptions().contains("not")) {
+            fragments.addSql("not");
+        }
+        fragments.addSql("exists(select 1 from dev_device_test _test where _test.id =", columnFullName);
+
+        RDBTableMetadata metadata = column
+            .getOwner()
+            .getSchema()
+            .getTable("dev_device_test")
+            .orElseThrow(() -> new UnsupportedOperationException("unsupported dev-alarm"));
+        SqlFragments where = builder.createTermFragments(metadata, terms);
+
+        if (!where.isEmpty()) {
+            fragments.addSql("and")
+                     .addFragments(where);
+
+        }
+        fragments.addSql(")");
+        return fragments;
+    }
+
+    static CompanyUserDetailTermBuilder builder = new CompanyUserDetailTermBuilder();
+
+    static class CompanyUserDetailTermBuilder extends AbstractTermsFragmentBuilder<TableOrViewMetadata> {
+
+        @Override
+        protected SqlFragments createTermFragments(TableOrViewMetadata parameter, List<Term> terms) {
+            return super.createTermFragments(parameter, terms);
+        }
+
+        @Override
+        protected SqlFragments createTermFragments(TableOrViewMetadata table, Term term) {
+            if (term.getValue() instanceof NativeSql) {
+                NativeSql sql = ((NativeSql) term.getValue());
+                return PrepareSqlFragments.of(sql.getSql(), sql.getParameters());
+            }
+            return table
+                .getColumn(term.getColumn())
+                .flatMap(column -> table
+                    .findFeature(TermFragmentBuilder.createFeatureId(term.getTermType()))
+                    .map(termFragment -> termFragment.createFragments(column.getFullName("_test"), column, term)))
+                .orElse(EmptySqlFragments.INSTANCE);
+        }
+    }
+}

+ 180 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/unit/UnitExcelInfo.java

@@ -0,0 +1,180 @@
+package org.jetlinks.pro.cqfire.service.unit;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.reactor.excel.CellDataType;
+import org.hswebframework.reactor.excel.ExcelHeader;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.pro.cqfire.entity.UnitEntity;
+import org.jetlinks.pro.cqfire.enums.Level;
+import org.jetlinks.pro.cqfire.enums.SocialUnitProcessStatus;
+import reactor.core.publisher.Flux;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+
+@Getter
+@Setter
+@Slf4j
+public class UnitExcelInfo {
+
+    @Schema(description = "单位项目名称")
+    private String name;
+
+    @Schema(description = "详细地址")
+    private String address;
+
+    @Schema(description = "重点单位等级(1:一级;2:二级;3:三级)")
+    private Level grade;
+
+    @Schema(description = "是否高危单位(1:是;0:否)")
+    private boolean isDanger;
+
+    @Schema(description = "企业名称")
+    private String enterpriseName;
+
+    @Schema(description = "统一社会编码")
+    private String enterpriseCode;
+
+    @Schema(description = "所在街道")
+    private String street;
+
+    @Schema(description = "楼层")
+    private String floor;
+
+    @Schema(description = "经营面积")
+    private Double businessArea;
+
+    @Schema(description = "固定资产")
+    private Double assets;
+
+    @Schema(description = "单位性质")
+    private String nature;
+
+    @Schema(description = "所属行业")
+    private String industry;
+
+    @Schema(description = "是否具有消控室(1:是;0:否)")
+    private boolean isFireControl;
+
+    @Schema(description = "区域Id")
+    private String regionId;
+
+    @Schema(description = "街道id")
+    private String streetId;
+
+    @Schema(description = "联系人姓名")
+    private String contactsName;
+
+    @Schema(description = "联系人电话")
+    private String contactsPhone;
+
+    @Schema(description = "联网情况(1:是;0:否)")
+    private boolean isNetWorking;
+
+    @Schema(description = "审核状态:1.待审核2.已驳回3.已审核")
+    private SocialUnitProcessStatus auditStatus;
+
+    @Schema(description = "审核拒绝原因")
+    private String rejectReason;
+
+    @Schema(description = "备注信息")
+    private String remark;
+
+    @Schema(description = "创建人")
+    private String fileCreateBy;
+
+    @Schema(description = "创建时间")
+    private String fileCreateTime;
+
+    @Schema(description = "最后修改人")
+    private String fileUpdateBy;
+
+    @Schema(description = "最后修改时间")
+    private String fileUpdateTime;
+
+    @Schema(description = "注册时间")
+    private String registerTime;
+
+    public UnitExcelInfo(UnitEntity unitEntity) {
+        this.name = unitEntity.getName();
+        this.address = unitEntity.getAddress();
+        this.grade = unitEntity.getGrade();
+        this.isDanger = unitEntity.getDanger();
+        this.enterpriseName = unitEntity.getEnterpriseName();
+        this.enterpriseCode = unitEntity.getEnterpriseCode();
+        this.street = unitEntity.getStreet();
+        this.floor = unitEntity.getFloor();
+        this.businessArea = unitEntity.getBusinessArea();
+        this.assets = unitEntity.getAssets();
+        this.nature = unitEntity.getNature();
+        this.industry = unitEntity.getIndustry();
+        this.isFireControl = unitEntity.getFireControl();
+        this.regionId = unitEntity.getRegionId();
+        this.streetId = unitEntity.getStreetId();
+        this.contactsName = unitEntity.getContactsName();
+        this.contactsPhone = unitEntity.getContactsPhone();
+        this.isNetWorking = unitEntity.getNetWorking();
+        this.auditStatus = unitEntity.getAuditStatus();
+        this.rejectReason = unitEntity.getRejectReason();
+        this.remark = unitEntity.getRemark();
+        this.fileCreateBy = unitEntity.getFileCreateBy();
+        this.fileUpdateBy = unitEntity.getFileUpdateBy();
+
+        if(unitEntity.getFileCreateTime() != null){
+            this.fileCreateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(unitEntity.getFileCreateTime()));
+        }
+        if(unitEntity.getFileUpdateTime() != null){
+            this.fileUpdateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(unitEntity.getFileUpdateTime()));
+        }
+        if(unitEntity.getRegisterTime() != null){
+            this.registerTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(unitEntity.getRegisterTime()));
+        }
+    }
+
+    public static List<ExcelHeader> getHeaderMapping() {
+        return new ArrayList<>(Arrays.asList(
+            new ExcelHeader("name", "单位项目名称", CellDataType.STRING),
+            new ExcelHeader("address", "详细地址", CellDataType.STRING),
+            new ExcelHeader("grade", "重点单位等级", CellDataType.STRING),
+            new ExcelHeader("isDanger", "是否高危单位", CellDataType.STRING),
+            new ExcelHeader("enterpriseName", "企业名称", CellDataType.STRING),
+            new ExcelHeader("enterpriseCode", "统一社会编码", CellDataType.STRING),
+            new ExcelHeader("street", "所在街道", CellDataType.STRING),
+            new ExcelHeader("floor", "楼层", CellDataType.STRING),
+            new ExcelHeader("businessArea", "经营面积", CellDataType.STRING),
+            new ExcelHeader("assets", "固定资产", CellDataType.STRING),
+            new ExcelHeader("nature", "单位性质", CellDataType.STRING),
+            new ExcelHeader("industry", "所属行业", CellDataType.STRING),
+            new ExcelHeader("isFireControl", "是否具有消控室", CellDataType.STRING),
+            new ExcelHeader("regionId", "区域Id", CellDataType.STRING),
+            new ExcelHeader("streetId", "街道id", CellDataType.STRING),
+            new ExcelHeader("contactsName", "联系人姓名", CellDataType.STRING),
+            new ExcelHeader("contactsPhone", "联系人电话", CellDataType.STRING),
+            new ExcelHeader("isNetWorking", "联网情况", CellDataType.STRING),
+            new ExcelHeader("auditStatus", "审核状态", CellDataType.STRING),
+            new ExcelHeader("rejectReason", "审核拒绝原因", CellDataType.STRING),
+            new ExcelHeader("remark", "备注信息", CellDataType.STRING),
+            new ExcelHeader("fileCreateBy", "创建人", CellDataType.STRING),
+            new ExcelHeader("fileCreateTime", "创建时间", CellDataType.STRING),
+            new ExcelHeader("fileUpdateBy", "最后修改人", CellDataType.STRING),
+            new ExcelHeader("fileUpdateTime", "最后修改时间", CellDataType.STRING),
+            new ExcelHeader("registerTime", "注册时间", CellDataType.STRING)
+        ));
+    }
+
+    public static Flux<UnitExcelInfo> getContentMapping(Flux<UnitEntity> entitys) {
+        return entitys.flatMap(log -> Flux.just(new UnitExcelInfo(log)))
+                      .doOnError(err -> log.error(err.getMessage(), err));
+    }
+
+    public Map<String, Object> toMap() {
+        return FastBeanCopier.copy(this, new HashMap<>(25));
+    }
+
+
+}
+

+ 17 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/unit/UnitService.java

@@ -0,0 +1,17 @@
+package org.jetlinks.pro.cqfire.service.unit;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.pro.cqfire.entity.UnitEntity;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class UnitService extends GenericReactiveCrudService<UnitEntity, String> {
+
+
+
+
+}

+ 187 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/service/user/UserService.java

@@ -0,0 +1,187 @@
+package org.jetlinks.pro.cqfire.service.user;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.hswebframework.web.api.crud.entity.TransactionManagers;
+import org.hswebframework.web.authorization.DefaultDimensionType;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.exception.NotFoundException;
+import org.hswebframework.web.system.authorization.api.PasswordEncoder;
+import org.hswebframework.web.system.authorization.api.PasswordValidator;
+import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity;
+import org.hswebframework.web.system.authorization.api.entity.UserEntity;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService;
+import org.hswebframework.web.system.authorization.defaults.service.DefaultReactiveUserService;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.pro.auth.entity.UserDetail;
+import org.jetlinks.pro.auth.entity.UserDetailEntity;
+import org.jetlinks.pro.auth.service.UserDetailService;
+import org.jetlinks.pro.cqfire.handle.VerifyCodeEvent;
+import org.jetlinks.pro.cqfire.util.RandomUtil;
+import org.jetlinks.pro.cqfire.web.code.VerificationCodeProperties;
+import org.jetlinks.pro.cqfire.web.user.RegisterRequest;
+import org.jetlinks.pro.cqfire.web.user.VerifyRequest;
+import org.jetlinks.pro.device.enums.PlatformType;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@AllArgsConstructor
+@Service
+public class UserService {
+    private final UserDetailService userDetailService;
+    private final DefaultReactiveUserService defaultReactiveUserService;
+    private final DefaultDimensionUserService dimensionUserService;
+    private final PasswordEncoder passwordEncoder = (password, salt) -> DigestUtils.md5Hex(String.format("hsweb.%s.framework.%s", password, salt));
+    private final PasswordValidator passwordValidator = (password) -> {
+    };
+
+    private final ApplicationEventPublisher eventPublisher;
+    private final ReactiveRedisOperations<String, String> redis;
+
+    private final VerificationCodeProperties properties;
+
+    private final static String dimensionId = "check-corp-role";
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Boolean> changePassword(String userId, String newPassword) {
+
+        passwordValidator.validate(newPassword);
+        return defaultReactiveUserService.findById(userId)
+                                         .switchIfEmpty(Mono.error(NotFoundException::new))
+                                         .flatMap(user -> defaultReactiveUserService
+                                             .createUpdate()
+                                             .set(UserEntity::getPassword, passwordEncoder.encode(newPassword, user.getSalt()))
+                                             .where(user::getId)
+                                             .execute())
+                                         .map(i -> i > 0);
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Boolean> resetPassword(String username, String telephone, String newPassword) {
+        passwordValidator.validate(newPassword);
+        return defaultReactiveUserService
+            .createQuery()
+            .where()
+            .and(UserEntity::getUsername, username)
+            .fetchOne()
+            .switchIfEmpty(Mono.error(new NotFoundException("该用户不存在!")))
+            .flatMap(user -> userDetailService
+                .findById(user.getId())
+                .flatMap(userDetail -> {
+                    if (!telephone.equals(userDetail.getTelephone())) {
+                        return Mono.error(new BusinessException("该手机号码和用户不匹配!"));
+                    }
+                    return defaultReactiveUserService
+                        .createUpdate()
+                        .set(UserEntity::getPassword, passwordEncoder.encode(newPassword, user.getSalt()))
+                        .where(user::getId)
+                        .execute()
+                        .map(i -> i > 0);
+                })
+            );
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Boolean> updatePassword(String userId, String oldPassword, String newPassword) {
+        passwordValidator.validate(newPassword);
+        return defaultReactiveUserService
+            .findById(userId)
+            .switchIfEmpty(Mono.error(NotFoundException::new))
+            .filter(user -> passwordEncoder.encode(oldPassword, user.getSalt()).equals(user.getPassword()))
+            .switchIfEmpty(Mono.error(new BusinessException("旧密码输入错误!")))
+            .flatMap(userEntity -> defaultReactiveUserService
+                .createUpdate()
+                .set(UserEntity::getPassword, passwordEncoder.encode(newPassword, userEntity.getSalt()))
+                .where(userEntity::getId)
+                .execute())
+            .map(i -> i > 0);
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<UserDetail> createUser(RegisterRequest request) {
+        ValidatorUtils.tryValidate(request);
+
+        return Mono
+            .defer(() -> {
+                UserEntity userEntity = request.toUserEntity();
+                return defaultReactiveUserService
+                    .saveUser(Mono.just(userEntity))
+                    .flatMap(b -> {
+                        UserDetailEntity userDetailEntity = new UserDetailEntity();
+                        userDetailEntity.copyFrom(request);
+                        userDetailEntity.setId(userEntity.getId());
+                        userDetailEntity.setName(userEntity.getName());
+                        //设置该用户属于哪个平台
+                        userDetailEntity.setDescription(PlatformType.test.name());
+                        return userDetailService
+                            .save(Mono.just(userDetailEntity))
+                            .then(userDetailService.findUserDetail(userDetailEntity.getId()));
+                    })
+                    .flatMap(res -> {
+                        DimensionUserEntity dimensionUserEntity = new DimensionUserEntity();
+                        dimensionUserEntity.setUserId(res.getId());
+                        dimensionUserEntity.setUserName(res.getName());
+                        dimensionUserEntity.setDimensionId(dimensionId);
+                        dimensionUserEntity.setDimensionTypeId(DefaultDimensionType.role.getId());
+                        dimensionUserEntity.setDimensionName("企业待审核角色");
+                        dimensionUserEntity.generateId();
+                        return dimensionUserService
+                            .save(Mono.just(dimensionUserEntity))
+                            .then(userDetailService.findUserDetail(res.getId()));
+                    });
+            })
+            ;
+    }
+
+    @Transactional(rollbackFor = Exception.class, transactionManager = TransactionManagers.reactiveTransactionManager)
+    public Mono<Integer> updatePhone(String userId, String newPhone) {
+        return userDetailService
+            .createUpdate()
+            .where(UserDetailEntity::getId, userId)
+            .set(UserDetailEntity::getTelephone, newPhone)
+            .execute();
+    }
+
+    public Mono<String> verifyCodeAndUsername(VerifyRequest request) {
+        ValidatorUtils.tryValidate(request);
+        return defaultReactiveUserService
+            .createQuery()
+            .where()
+            .and(UserEntity::getUsername, request.getUsername())
+            .fetchOne()
+            .switchIfEmpty(Mono.error(new NotFoundException("用户[" + request.getUsername() + "]不存在")))
+            .flatMap(userEntity -> userDetailService
+                .findById(userEntity.getId())
+                .flatMap(userDetailEntity -> {
+                    if (!request.getTelephone().equals(userDetailEntity.getTelephone())) {
+                        return Mono.error(new BusinessException("账号和手机号码不匹配"));
+                    }
+                    VerifyCodeEvent verifyCodeEvent = new VerifyCodeEvent(request.getToken(),
+                                                                          request.getCode(),
+                                                                          request.getTelephone());
+                    return verifyCodeEvent
+                        .publish(eventPublisher)
+                        .then(Mono.defer(() -> {
+                            String token = RandomUtil.getCacheKey(request.getTelephone(), request
+                                .getToken());
+                            return redis
+                                .opsForValue()
+                                .set("verify:" + RandomUtil.getCacheKey(request.getTelephone(), token),
+                                     request.getUsername(),
+                                     Duration.ofSeconds(properties.getTermOfValidity()))
+                                .thenReturn(token);
+                        }));
+                })
+            );
+    }
+}

+ 66 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestProvider.java

@@ -0,0 +1,66 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import com.alibaba.fastjson.JSONObject;
+import org.jetlinks.core.event.EventBus;
+import org.jetlinks.core.event.Subscription;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.jetlinks.pro.notify.manager.subscriber.Notify;
+import org.jetlinks.pro.notify.manager.subscriber.Subscriber;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Component
+public class DeviceTestProvider implements SubscriberProvider {
+
+    private final EventBus eventBus;
+
+    public DeviceTestProvider(EventBus eventBus) {
+        this.eventBus = eventBus;
+    }
+
+    private Flux<Notify> createSubscribe(String id,
+                                         String[] topics) {
+        Subscription.Feature[] features = new Subscription.Feature[]{Subscription.Feature.local};
+
+        return Flux
+            .defer(() -> this
+                .eventBus
+                .subscribe(Subscription.of("device-test:" + id, topics, features))
+                .map(msg -> {
+                    JSONObject json = msg.bodyToJson();
+                    return Notify.of(
+                        String.format("设备[%s]正在测试!", json.getString("deviceName")),
+                        json.getString("testId"),
+                        System.currentTimeMillis()
+                    );
+                }));
+    }
+
+    @Override
+    public Mono<Subscriber> createSubscriber(String id, List<ReportTestInfo> testInfos) {
+
+        List<String> topicList = new ArrayList<>();
+        for (ReportTestInfo info : testInfos) {
+            topicList.add(createTopic(info.getProductId(), info.getDeviceId(), info.getReportId()));
+        }
+        return Flux.fromIterable(topicList)
+                   .collectList()
+                   .map(topics -> () -> createSubscribe(id, topics.toArray(new String[0])));
+    }
+
+    private String createTopic(String productId,
+                               String deviceId,
+                               String reportId) {
+        return String.format("/device-test/device/testing/%s/%s/%s", productId, deviceId, reportId);
+    }
+}

+ 412 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestRule.java

@@ -0,0 +1,412 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.core.message.DeviceMessage;
+import org.jetlinks.core.message.function.FunctionInvokeMessage;
+import org.jetlinks.core.message.function.FunctionParameter;
+import org.jetlinks.core.message.property.ReadPropertyMessage;
+import org.jetlinks.pro.rule.engine.device.ShakeLimit;
+import org.jetlinks.reactor.ql.utils.CastUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+public class DeviceTestRule {
+
+
+    /**
+     * 规则ID
+     */
+    @Hidden
+    private String id;
+
+    /**
+     * 产品ID,不能为空
+     */
+    @NotBlank
+    @Schema(description = "产品ID")
+    private String productId;
+
+    /**
+     * 产品名称,不能为空
+     */
+    @Schema(description = "产品名称")
+    private String productName;
+
+    /**
+     * 设备ID,当对特定对设备设置规则时,不能为空
+     */
+    @Schema(description = "设备ID")
+    private String deviceId;
+
+    /**
+     * 多个设备ID,只处理产品下指定设备列表的数据
+     *
+     * @since 1.11
+     */
+    @Schema(description = "多个设备ID,只处理产品下指定设备列表的数据.")
+    private List<String> deviceIdList;
+
+    /**
+     * 设备名称
+     */
+    @Schema(description = "设备名称")
+    private String deviceName;
+
+    /**
+     * 触发条件,不能为空
+     */
+    @Schema(description = "触发条件")
+    private List<Trigger> triggers;
+
+    /**
+     * 要单独获取哪些字段信息
+     */
+    @Schema(description = "自定义字段映射")
+    private List<DeviceTestRule.Property> properties;
+
+    /**
+     * 防抖限制
+     */
+    @Schema(description = "防抖限制")
+    private ShakeLimit shakeLimit;
+
+    @AllArgsConstructor
+    @Getter
+    public enum MessageType {
+        //上线
+        online("/device/%s/%s/online", "this.") {
+            @Override
+            public String getTopic(String productId, String deviceId, String key) {
+                return String.format(getTopicTemplate(), productId, StringUtils.isEmpty(deviceId) ? "*" : deviceId);
+            }
+
+            @Override
+            public ConditionFilter createOperator(String key, Operator operator, String value, String parameter) {
+                return null;
+            }
+        },
+        //离线
+        offline("/device/%s/%s/offline", "this.") {
+            @Override
+            public String getTopic(String productId, String deviceId, String key) {
+                return String.format(getTopicTemplate(), productId, StringUtils.isEmpty(deviceId) ? "*" : deviceId);
+            }
+
+            @Override
+            public ConditionFilter createOperator(String key, Operator operator, String value, String parameter) {
+                return null;
+            }
+        },
+        //属性
+        properties("/device/%s/%s/message/property/**", "this.properties.") {
+            @Override
+            public String getTopic(String productId, String deviceId, String key) {
+                return String.format(getTopicTemplate(), productId, StringUtils.isEmpty(deviceId) ? "*" : deviceId);
+            }
+
+            @Override
+            public ConditionFilter createOperator(String key, Operator operator, String value, String parameter) {
+
+                return new ConditionFilter(key, value, operator);
+            }
+
+            @Override
+            public Optional<DeviceMessage> createMessage(Trigger trigger) {
+                ReadPropertyMessage readPropertyMessage = new ReadPropertyMessage();
+                readPropertyMessage.setProperties(new ArrayList<>(
+                    StringUtils.hasText(trigger.getModelId())
+                        ? Collections.singletonList(trigger.getModelId())
+                        : Collections.emptyList()));
+
+                return Optional.of(readPropertyMessage);
+            }
+        },
+        //事件
+        event("/device/%s/%s/message/event/%s", "this.data.") {
+            @Override
+            public String getTopic(String productId, String deviceId, String property) {
+                return String.format(getTopicTemplate(), productId, StringUtils.isEmpty(deviceId) ? "*" : deviceId, property);
+            }
+
+            @Override
+            public ConditionFilter createOperator(String key, Operator operator, String value, String parameter) {
+                return new ConditionFilter(parameter, value, operator);
+            }
+        },
+        //功能调用回复
+        function("/device/%s/%s/message/function/reply", "this.output.") {
+            @Override
+            public String getTopic(String productId, String deviceId, String property) {
+                return String.format(getTopicTemplate(), productId, StringUtils.isEmpty(deviceId) ? "*" : deviceId);
+            }
+
+            @Override
+            public ConditionFilter createOperator(String key, Operator operator, String value, String parameter) {
+                return null;
+            }
+
+            @Override
+            public Optional<DeviceMessage> createMessage(Trigger trigger) {
+                FunctionInvokeMessage message = new FunctionInvokeMessage();
+                message.setFunctionId(trigger.getModelId());
+                message.setInputs(trigger.getParameters());
+                message.setTimestamp(System.currentTimeMillis());
+                return Optional.of(message);
+            }
+        };
+
+        private final String topicTemplate;
+
+        private final String propertyPrefix;
+
+        public abstract String getTopic(String productId, String deviceId, String key);
+
+        public abstract ConditionFilter createOperator(String key, Operator operator, String value, String parameter);
+
+        public Optional<DeviceMessage> createMessage(Trigger trigger) {
+            return Optional.empty();
+        }
+
+        public static MessageType getType(String type) {
+            for (MessageType value : MessageType.values()) {
+                if (value.name().equals(type)) {
+                    return value;
+                }
+            }
+            throw new BusinessException("类型" + type + "不存在!");
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class Trigger implements Serializable {
+
+        //trigger为定时任务时的cron表达式
+        @Schema(description = "定时触发cron表达式")
+        private String cron;
+
+        //类型,属性或者事件.
+        @Schema(description = "触发消息类型")
+        private MessageType type;
+
+        //trigger为定时任务并且消息类型为功能调用时
+        @Schema(description = "定时调用下发功能指令时的参数")
+        private List<FunctionParameter> parameters;
+
+        //物模型属性或者事件的标识 如: fire_alarm
+        @Schema(description = "物模型标识,如:属性ID,事件ID")
+        private String modelId;
+
+        @Schema(description = "测试项id")
+        private String testItemId;
+
+        //过滤条件
+        @Schema(description = "条件")
+        private List<ConditionFilter> filters;
+
+        public Set<String> toColumns() {
+            Set<String> columns = new LinkedHashSet<>();
+            columns.add(type.getPropertyPrefix() + "this $this");
+
+            if (StringUtils.hasText(modelId)) {
+                //this.properties.this['temp'] temp
+                columns.add(
+                    type.getPropertyPrefix() + "this['" + modelId + "'] \"" + modelId + "\""
+                );
+            }
+            if (!CollectionUtils.isEmpty(filters)) {
+                for (ConditionFilter filter : filters) {
+                    columns.add(filter.getColumn(type));
+                }
+            }
+
+            return columns;
+        }
+
+        public List<Object> toFilterBinds() {
+            return filters == null ? Collections.emptyList() :
+                filters.stream()
+                       .map(ConditionFilter::convertValue)
+                       .collect(Collectors.toList());
+        }
+
+        public Optional<String> createExpression() {
+            if (CollectionUtils.isEmpty(filters)) {
+                return Optional.empty();
+            }
+            return Optional.of(
+                filters.stream()
+                       .map(filter -> filter.createExpression(type))
+                       .collect(Collectors.joining(" and "))
+            );
+        }
+
+        public void validate() {
+            if (type == null) {
+                throw new IllegalArgumentException("error.device_alarm_trigger_type_cannot_be_empty");
+            }
+
+            if (type != MessageType.online && type != MessageType.offline && StringUtils.isEmpty(modelId)) {
+                throw new IllegalArgumentException("error.property_event_function_id_cannot_be_empty");
+            }
+            if (!CollectionUtils.isEmpty(filters)) {
+                filters.forEach(ConditionFilter::validate);
+            }
+        }
+
+        public String toSQL(int index, List<String> defaultColumns, List<Property> properties) {
+            List<String> columns = new ArrayList<>(defaultColumns);
+            List<String> wheres = new ArrayList<>();
+
+            // select this.properties.this trigger0
+            columns.add(getType().getPropertyPrefix() + "this trigger" + index);
+            columns.addAll(toColumns());
+            createExpression()
+                .ifPresent(expr -> wheres.add("(" + expr + ")"));
+
+            String sql = "select \n\t\t" + String.join("\n\t\t,", columns) + " \n\tfrom dual ";
+
+            if (!wheres.isEmpty()) {
+                sql += "\n\twhere " + String.join("\n\t\t or ", wheres);
+            }
+
+            if (org.apache.commons.collections.CollectionUtils.isNotEmpty(properties)) {
+                List<String> newColumns = new ArrayList<>(defaultColumns);
+                for (Property property : properties) {
+                    if (StringUtils.isEmpty(property.getProperty())) {
+                        continue;
+                    }
+                    String alias = StringUtils.hasText(property.getAlias()) ? property.getAlias() : property.getProperty();
+                    // 'message',func(),this[name]
+                    if ((property.getProperty().startsWith("'") && property.getProperty().endsWith("'"))
+                        ||
+                        property.getProperty().contains("(") || property.getProperty().contains("[")) {
+                        newColumns.add(property.getProperty() + " \"" + alias + "\"");
+                    } else {
+                        newColumns.add("this['" + property.getProperty() + "'] \"" + alias + "\"");
+                    }
+                }
+                if (newColumns.size() > defaultColumns.size()) {
+                    sql = "select \n\t" + String.join("\n\t,", newColumns) + "\n from (\n\t" + sql + "\n) t";
+                }
+            }
+            return sql;
+        }
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ConditionFilter implements Serializable {
+        //过滤条件key 如: temperature
+        @Schema(description = "条件key")
+        private String key;
+
+        //过滤条件值
+        @Schema(description = "值")
+        private String value;
+
+        //操作符, 等于,大于,小于....
+        @Schema(description = "比对方式")
+        private Operator operator = Operator.eq;
+
+        public String getColumn(MessageType type) {
+            return type.getPropertyPrefix() + "this['" + (key.trim()) + "'] \"" + (key.trim()) + "\"";
+        }
+
+        public String createExpression(MessageType type) {
+            return createExpression(type, true);
+        }
+
+        public String createExpression(MessageType type, boolean prepareSQL) {
+            //函数和this忽略前缀
+            if (key.contains("(") || key.startsWith("this")) {
+                return key + operator.symbol + " ? ";
+            }
+            return type.getPropertyPrefix() + "this['" + (key.trim()) + "'] " + operator.symbol
+                + " " + (prepareSQL ? "? " : valueIsExpression() ? value : "'" + value + "'");
+        }
+
+        public boolean valueIsExpression() {
+            return false;
+        }
+
+        public Object convertValue() {
+            return operator.convert(value);
+        }
+
+        public void validate() {
+            if (StringUtils.isEmpty(key)) {
+                throw new IllegalArgumentException("error.condition_key_cannot_be_empty");
+            }
+            if (StringUtils.isEmpty(value)) {
+                throw new IllegalArgumentException("error.condition_value_cannot_be_empty");
+            }
+        }
+    }
+
+
+    @AllArgsConstructor
+    @Getter
+    public enum Operator {
+        eq("="),
+        not("!="),
+        gt(">"),
+        lt("<"),
+        gte(">="),
+        lte("<="),
+        like("like") {
+            @Override
+            public Object convert(String value) {
+                if (value.contains("%")) {
+                    return super.convert(value);
+                }
+                return "%" + value + "%";
+            }
+        };
+        private final String symbol;
+
+        public Object convert(String value) {
+            if (org.hswebframework.utils.StringUtils.isNumber(value)) {
+                return CastUtils.castNumber(value);
+            }
+            return value;
+        }
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Property implements Serializable {
+        @Schema(description = "属性")
+        private String property;
+
+        @Schema(description = "别名")
+        private String alias;
+
+        @Override
+        public String toString() {
+            return property.concat(" \"").concat(StringUtils.hasText(alias) ? alias : property).concat("\"");
+        }
+    }
+}

+ 215 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/DeviceTestTaskExecutor.java

@@ -0,0 +1,215 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql;
+import org.jetlinks.core.event.EventBus;
+import org.jetlinks.core.event.Subscription;
+import org.jetlinks.core.message.DeviceMessage;
+import org.jetlinks.core.metadata.Jsonable;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.cqfire.entity.TestItemEntity;
+import org.jetlinks.pro.cqfire.enums.ReportTestState;
+import org.jetlinks.pro.cqfire.enums.TestState;
+import org.jetlinks.pro.cqfire.service.item.TestItemService;
+import org.jetlinks.pro.cqfire.service.report.ReportService;
+import org.jetlinks.reactor.ql.ReactorQL;
+import org.jetlinks.reactor.ql.ReactorQLContext;
+import org.jetlinks.reactor.ql.ReactorQLRecord;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Slf4j
+public class DeviceTestTaskExecutor {
+
+    /**
+     * 默认要查询的列
+     */
+    static List<String> default_columns = Arrays.asList(
+        //时间戳
+        "this.timestamp timestamp",
+        //设备ID
+        "this.deviceId deviceId",
+        //header
+        "this.headers headers",
+        //设备名称,通过DeviceMessageConnector自定填充了值
+        "this.headers.deviceName deviceName",
+        //消息唯一ID
+        "this.headers._uid _uid",
+        //消息类型,下游可以根据消息类型来做处理,比如:离线时,如果网关设备也不在线则不触发.
+        "this.messageType messageType"
+    );
+    private final EventBus eventBus;
+    private final ReportService reportService;
+    private final TestItemService testItemService;
+
+
+    //触发器对应的ReactorQL缓存
+    private final Map<DeviceTestRule.Trigger, ReactorQL> triggerQL = new ConcurrentHashMap<>();
+
+    private DeviceTestRule rule;
+
+
+    public DeviceTestTaskExecutor(EventBus eventBus,
+                                  ReportService reportService,
+                                  TestItemService testItemService,
+                                  DeviceTestRule rule) {
+        this.eventBus = eventBus;
+        this.reportService = reportService;
+        this.testItemService = testItemService;
+        this.rule = rule;
+        triggerQL.putAll(createQL(rule));
+    }
+
+    static ReactorQL createQL(int index, DeviceTestRule.Trigger trigger, DeviceTestRule rule) {
+        String sql = trigger.toSQL(index, default_columns, rule.getProperties());
+        log.debug("create device test sql : \n{}", sql);
+        return ReactorQL.builder().sql(sql).build();
+    }
+
+    private Map<DeviceTestRule.Trigger, ReactorQL> createQL(DeviceTestRule rule) {
+        Map<DeviceTestRule.Trigger, ReactorQL> qlMap = new HashMap<>();
+        int index = 0;
+        for (DeviceTestRule.Trigger trigger : rule.getTriggers()) {
+            qlMap.put(trigger, createQL(index++, trigger, rule));
+        }
+        return qlMap;
+    }
+
+    public Flux<Map<String, Object>> doSubscribe() {
+
+        List<Flux<? extends Map<String, Object>>> triggerOutputs = new ArrayList<>();
+
+        int index = 0;
+
+        for (DeviceTestRule.Trigger trigger : rule.getTriggers()) {
+            ReactorQL ql = triggerQL.get(trigger);
+            if (ql == null) {
+                log.warn("DeviceTestRule trigger {} init error", index);
+                continue;
+            }
+
+
+            Set<String> topics = new HashSet<>();
+            topics.add(trigger.getType().getTopic(rule.getProductId(), rule.getDeviceId(), trigger.getModelId()));
+            log.debug("device test report[{}] topics:{}", rule.getId(), topics);
+            Subscription subscription = Subscription.of(
+                "device_test:" + rule.getId() + ":" + index++,
+                topics.toArray(new String[0]),
+                Subscription.Feature.local
+            );
+
+            Flux<? extends Map<String, Object>> datasource = eventBus
+                .subscribe(subscription, DeviceMessage.class)
+                .map(Jsonable::toJson);
+
+            ReactorQLContext qlContext = ReactorQLContext
+                .ofDatasource((t) -> datasource
+                    .take(1)
+                    .flatMap(map -> {
+                        String topic = "/device-test/"
+                            + rule.getDeviceId()
+                            + "/"
+                            + trigger.getTestItemId()
+                            + "/complete";
+                        //更新报告表状态
+                        return autoUpdateReportState(rule.getId(),
+                                                     rule.getTriggers().size()
+                        )
+                            //更新测试项目表
+                            .then(updateTestItemState(trigger.getTestItemId()))
+                            //推送到eventbus  /device-test/设备id/项目id/complete,报告id
+                            .then(eventBus.publish(topic, rule.getId()))
+                            .thenReturn(map)
+                            ;
+                    })
+                    .doOnNext(map -> {
+                        if (StringUtils.hasText(rule.getDeviceName())) {
+                            map.putIfAbsent("deviceName", rule.getDeviceName());
+                        }
+                        if (StringUtils.hasText(rule.getProductName())) {
+                            map.putIfAbsent("productName", rule.getProductName());
+                        }
+                        map.put("productId", rule.getProductId());
+                        map.put("testId", rule.getId());
+                    }));
+            trigger.toFilterBinds().forEach(qlContext::bind);
+
+            triggerOutputs.add(ql.start(qlContext).map(ReactorQLRecord::asMap));
+        }
+
+        return Flux.merge(triggerOutputs)
+                   .flatMap(map -> {
+                       map.put("productId", rule.getProductId());
+                       map.put("testId", rule.getId());
+
+                       if (StringUtils.hasText(rule.getDeviceName())) {
+                           map.putIfAbsent("deviceName", rule.getDeviceName());
+                       }
+                       if (StringUtils.hasText(rule.getProductName())) {
+                           map.putIfAbsent("productName", rule.getProductName());
+                       }
+                       if (StringUtils.hasText(rule.getDeviceId())) {
+                           map.putIfAbsent("deviceId", rule.getDeviceId());
+                       }
+                       if (!map.containsKey("deviceName") && map.get("deviceId") != null) {
+                           map.putIfAbsent("deviceName", map.get("deviceId"));
+                       }
+                       if (!map.containsKey("productName")) {
+                           map.putIfAbsent("productName", rule.getProductId());
+                       }
+                       if (log.isDebugEnabled()) {
+                           log.debug("设备测试中:{}", map);
+                       }
+                       return Mono.just(map);
+                   });
+
+    }
+
+    public Mono<Integer> autoUpdateReportState(String reportId, int items) {
+        //update set state='合格',通过项目数量=通过项目数量+1 where id = xx and test_items=测试项目数量-1
+        return reportService.createUpdate()
+                            .set(ReportEntity::getState,
+                                 NativeSql.of("case when passed_items= ? then ? " +
+                                                  "else state end," +
+                                                  "passed_items=passed_items+1",
+                                              items - 1,
+                                              "qualified"))
+                            .set(ReportEntity::getPassTime, System.currentTimeMillis())
+                            .where()
+                            .and(ReportEntity::getId, reportId)
+                            .not(ReportEntity::getState, ReportTestState.qualified)
+                            .execute()
+                            .doOnNext(res -> {
+                                if (res > 0) {
+                                    log.debug("设备{}测试报告已合格", rule.getDeviceName());
+                                }
+                            })
+            ;
+    }
+
+    public Mono<Integer> updateTestItemState(String testItemId) {
+        return testItemService.createUpdate()
+                              .where()
+                              .and(TestItemEntity::getId, testItemId)
+                              .not(TestItemEntity::getState, TestState.passed)
+                              .set(TestItemEntity::getState, TestState.passed)
+                              .execute()
+            .flatMap(res -> {
+                if (res > 0) {
+                    log.debug("设备{}测试报告,测试项[{}]已通过", rule.getDeviceName(), testItemId);
+                } else {
+                    log.warn("设备{}测试报告,测试项[{}]通过未修改成功", rule.getDeviceName(), testItemId);
+                }
+                return Mono.just(res);
+            });
+    }
+}

+ 24 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/ReportTestInfo.java

@@ -0,0 +1,24 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+@Builder
+public class ReportTestInfo {
+
+    private String reportId;
+
+    private String productId;
+
+    private String deviceId;
+
+    private String deviceName;
+}

+ 18 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/SubscriberProvider.java

@@ -0,0 +1,18 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.jetlinks.pro.notify.manager.subscriber.Subscriber;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+public interface SubscriberProvider {
+
+    Mono<Subscriber> createSubscriber(String id, List<ReportTestInfo> testInfos);
+}

+ 22 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/subscriber/TestItemCompletedEvent.java

@@ -0,0 +1,22 @@
+package org.jetlinks.pro.cqfire.subscriber;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+@Builder
+public class TestItemCompletedEvent {
+
+    private String deviceId;
+
+    private String testItemId;
+
+    private String reportId;
+}

+ 23 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/util/Md5Util.java

@@ -0,0 +1,23 @@
+package org.jetlinks.pro.cqfire.util;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.security.MessageDigest;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+public class Md5Util {
+
+    public static String getMd5String(String... key) {
+
+        MessageDigest digest = DigestUtils.getMd5Digest();
+        for (String s : key) {
+            digest.update(s.getBytes());
+        }
+        return Hex.encodeHexString(digest.digest());
+    }
+}

+ 32 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/util/RandomUtil.java

@@ -0,0 +1,32 @@
+package org.jetlinks.pro.cqfire.util;
+
+import lombok.SneakyThrows;
+import org.apache.commons.codec.binary.Hex;
+
+import java.security.MessageDigest;
+import java.util.UUID;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+public class RandomUtil {
+
+    public static String getCode() {
+        return String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
+    }
+
+    public static String getToken() {
+        return UUID.randomUUID().toString();
+    }
+
+    @SneakyThrows
+    public static String getCacheKey(String receiver, String token) {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        md.update(receiver.getBytes());
+        md.update(token.getBytes());
+        return Hex.encodeHexString(md.digest());
+    }
+
+}

+ 43 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/ClusterInfoController.java

@@ -0,0 +1,43 @@
+package org.jetlinks.pro.cqfire.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.jetlinks.core.cluster.ServerNode;
+import org.jetlinks.pro.network.manager.web.ClusterController;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+/**
+ * @Deprecated
+ * @see org.jetlinks.pro.network.manager.web.ClusterController
+ */
+@RequestMapping("/cluster")
+@RestController
+@Authorize
+@Tag(name = "系统管理")
+@Deprecated
+public class ClusterInfoController {
+
+    @Autowired
+    private ClusterController clusterController;
+
+    @GetMapping("/nodes")
+    @Operation(summary = "获取集群节点")
+    public Flux<ServerNode> getServerNodes() {
+        return clusterController
+            .getServerNodes()
+            .map(node -> {
+                ServerNode serverNode = new ServerNode();
+                serverNode.setId(node.getAlias());
+                serverNode.setName(node.getAlias());
+                serverNode.setHost(node.getAddress());
+                serverNode.setLeader(node.isCurrent());
+                return serverNode;
+            });
+    }
+
+}

+ 31 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/SystemInfoController.java

@@ -0,0 +1,31 @@
+package org.jetlinks.pro.cqfire.web;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.jetlinks.pro.Version;
+import org.jetlinks.pro.cqfire.configuration.api.ApiInfoProperties;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RequestMapping("/system")
+@RestController
+@AllArgsConstructor
+public class SystemInfoController {
+
+    private final ApiInfoProperties addressProperties;
+
+    @GetMapping("/version")
+    @Authorize(ignore = true)
+    public Mono<Version> getVersion() {
+        return Mono.just(Version.current);
+    }
+
+    @GetMapping("/apis")
+    @Authorize(ignore = true)
+    public Mono<ApiInfoProperties> getApis() {
+        return Mono.just(addressProperties);
+    }
+
+}

+ 76 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/api/ApiController.java

@@ -0,0 +1,76 @@
+package org.jetlinks.pro.cqfire.web.api;
+
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.cqfire.enums.ReportTestState;
+import org.jetlinks.pro.cqfire.service.report.ReportService;
+import org.jetlinks.pro.device.entity.DeviceProductEntity;
+import org.jetlinks.pro.device.entity.DeviceTestEntity;
+import org.jetlinks.pro.device.service.DeviceTestService;
+import org.jetlinks.pro.device.service.LocalDeviceProductService;
+import org.jetlinks.pro.tenant.annotation.TenantAssets;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/api/product")
+@AllArgsConstructor
+@Tag(name = "远程监控产品查询")
+@Resource(id = "device-product", name = "设备产品")
+@Slf4j
+public class ApiController {
+
+    private final ReportService reportService;
+    private final DeviceTestService deviceTestService;
+    private final LocalDeviceProductService productService;
+
+    @GetMapping("/info/_query")
+    @QueryAction
+    @Operation(summary = "根据厂商、型号、类型、协议id获取测试通过的产品详情")
+    @TenantAssets(ignore = true)
+    public Mono<DeviceProductEntity> getProductInfo(@RequestParam @Parameter(description = "厂商名称") String corpName,
+                                                    @RequestParam @Parameter(description = "品类名称") String classifiedName,
+                                                    @RequestParam @Parameter(description = "设备型号") String model,
+                                                    @RequestParam(required = false) @Parameter(description = "消息协议ID") String messageProtocol) {
+        String productId;
+        if (StringUtils.isEmpty(messageProtocol)) {
+            productId = DigestUtils.md5Hex(corpName + classifiedName + model);
+        } else {
+            productId = DigestUtils.md5Hex(corpName + classifiedName + model + messageProtocol);
+        }
+        return deviceTestService
+            .createQuery()
+            .where(DeviceTestEntity::getProductId, productId)
+            .fetchOne()
+            .flatMap(testEntity -> {
+                if (StringUtils.isEmpty(testEntity.getSourceAddress())) {
+                    return Mono.error(new UnsupportedOperationException("产品未通过测试"));
+                }
+                return reportService
+                    .createQuery()
+                    .where(ReportEntity::getDeviceId, testEntity.getId())
+                    .and(ReportEntity::getState, ReportTestState.qualified)
+                    .count()
+                    .flatMap(num -> {
+                        if (num != 0) {
+                            return productService.findById(productId);
+                        }
+                        return Mono.error(new UnsupportedOperationException("产品未通过测试"));
+                    });
+            })
+            .switchIfEmpty(productService.findById(productId));
+    }
+
+}

+ 128 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/architecture/ArchitectureController.java

@@ -0,0 +1,128 @@
+package org.jetlinks.pro.cqfire.web.architecture;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.reactor.excel.ReactorExcel;
+import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.pro.cqfire.entity.ArchitectureEntity;
+import org.jetlinks.pro.cqfire.service.architecture.ArchitectureExcelInfo;
+import org.jetlinks.pro.cqfire.service.architecture.ArchitectureService;
+import org.jetlinks.pro.cqfire.web.unit.SocialUnitController;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+@RestController
+@RequestMapping("/architecture")
+@AllArgsConstructor
+@Tag(name = "建筑管理")
+@Resource(id = "architecture", name = "建筑管理")
+@Slf4j
+public class ArchitectureController implements ReactiveServiceCrudController<ArchitectureEntity, String> {
+
+    private final ArchitectureService architectureService;
+
+    private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
+
+    @Override
+    public ReactiveCrudService<ArchitectureEntity, String> getService() {
+        return architectureService;
+    }
+
+    @GetMapping("/download.{format}")
+    @QueryAction
+    @QueryNoPagingOperation(summary = "导出建筑")
+    public Mono<Void> downloadAccessLogger(ServerHttpResponse response,
+                                           @Parameter(hidden = true) QueryParamEntity queryParam,
+                                           @PathVariable @Parameter(description = "文件格式,支持csv,xlsx") String format) throws IOException {
+        response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION,
+                                  "attachment; filename=".concat(URLEncoder.encode("建筑管理." + format, StandardCharsets.UTF_8
+                                      .displayName())));
+        return Mono
+            .just(ArchitectureExcelInfo.getHeaderMapping())
+            .flatMapMany(headers -> ReactorExcel
+                .<ArchitectureExcelInfo>writer(format)
+                .headers(headers)
+                .converter(ArchitectureExcelInfo::toMap)
+                .writeBuffer(ArchitectureExcelInfo.getContentMapping(architectureService.query(queryParam)), 512 * 1024))
+            .doOnError(err -> log.error(err.getMessage(), err))
+            .map(bufferFactory::wrap)
+            .as(response::writeWith);
+
+    }
+
+    @Override
+    public Mono<SaveResult> save(Flux<ArchitectureEntity> payload) {
+        return Authentication
+            .currentReactive()
+            .flatMap(autz -> architectureService
+                .save(payload
+                          .flatMap(architectureEntity -> architectureService
+                              .findById(architectureEntity.getId())
+                              .switchIfEmpty(Mono.just(architectureEntity))
+                              .flatMap(oldArchitecture -> {
+                                  String newOrgId = architectureEntity.getOrganizationId();
+                                  String newManagerId = SocialUnitController.PREFIX + architectureEntity.getManagementUnitId();
+                                  if (StringUtils.isEmpty(oldArchitecture.getCreatorId())) {
+                                      //新建 一般来说此时不会有设备
+                                      return architectureService
+                                          .getDeviceId(architectureEntity.getId())
+                                          .flatMap(deviceIds -> architectureService
+                                              .bindDevice(newOrgId, deviceIds)
+                                              .then(architectureService.bindDevice(newManagerId, deviceIds)))
+                                          .thenReturn(architectureEntity);
+                                  }
+                                  String oldOrgId = oldArchitecture.getOrganizationId();
+                                  String oldManagerId = SocialUnitController.PREFIX + oldArchitecture.getManagementUnitId();
+                                  if (!Objects.equals(oldOrgId, newOrgId) && !Objects.equals(oldManagerId, newManagerId)) {
+                                      return architectureService
+                                          .getDeviceId(architectureEntity.getId())
+                                          .flatMap(deviceIds -> architectureService
+                                              .unBindDevice(oldOrgId, deviceIds)
+                                              .then(architectureService.bindDevice(newOrgId, deviceIds))
+                                              .then(architectureService
+                                                        .unBindDevice(oldManagerId, deviceIds)
+                                                        .then(architectureService.bindDevice(newManagerId, deviceIds))))
+                                          .thenReturn(architectureEntity);
+                                  } else if (!Objects.equals(oldOrgId, newOrgId)) {
+                                      return architectureService
+                                          .getDeviceId(architectureEntity.getId())
+                                          .flatMap(deviceIds -> architectureService
+                                              .unBindDevice(oldOrgId, deviceIds)
+                                              .then(architectureService.bindDevice(newOrgId, deviceIds)))
+                                          .thenReturn(architectureEntity);
+                                  } else if (!Objects.equals(oldManagerId, newManagerId)) {
+                                      return architectureService
+                                          .getDeviceId(architectureEntity.getId())
+                                          .flatMap(deviceIds -> architectureService
+                                              .unBindDevice(oldManagerId, deviceIds)
+                                              .then(architectureService.bindDevice(newManagerId, deviceIds)))
+                                          .thenReturn(architectureEntity);
+                                  }
+                                  return Mono.just(architectureEntity);
+                              }))
+                ));
+    }
+
+
+
+}

+ 38 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/ChangePasswordCodeValidator.java

@@ -0,0 +1,38 @@
+package org.jetlinks.pro.cqfire.web.code;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.pro.notify.DefaultNotifyType;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Setter
+@Getter
+public class ChangePasswordCodeValidator {
+
+    @Schema(description = "token")
+    @NotBlank(message = "token不能为空")
+    private String token;
+
+    @Schema(description = "验证码")
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+
+    @Schema(description = "接收人(电话号码、邮箱)")
+    @NotBlank(message = "接收人(电话号码、邮箱)不能为空")
+    private String receiver;
+
+    @Schema(description = "新密码")
+    @NotBlank(message = "新密码不能为空")
+    private String newPassword;
+
+    @Schema(description = "验证方式")
+    @NotBlank(message = "验证方式不能为空")
+    private DefaultNotifyType type;
+}

+ 30 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/CodeVerifyRequest.java

@@ -0,0 +1,30 @@
+package org.jetlinks.pro.cqfire.web.code;
+
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Setter
+@Getter
+public class CodeVerifyRequest {
+
+    @Schema(description = "token")
+    @NotBlank(message = "token不能为空")
+    private String token;
+
+    @ApiModelProperty(value = "验证码")
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+
+    @ApiModelProperty(value = "手机号码")
+    @NotBlank(message = "手机号码不能为空")
+    private String telephone;
+}

+ 146 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeController.java

@@ -0,0 +1,146 @@
+package org.jetlinks.pro.cqfire.web.code;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.pro.cqfire.handle.CodeValidatedEvent;
+import org.jetlinks.pro.cqfire.handle.VerifyCodeEvent;
+import org.jetlinks.pro.cqfire.util.RandomUtil;
+import org.jetlinks.pro.notify.NotifierManager;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
+import org.springframework.data.redis.core.ReactiveRedisOperations;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@RestController
+@Authorize(ignore = true)
+@AllArgsConstructor
+@RequestMapping("/authorize/code")
+@Tag(name = "验证码接口")
+public class VerificationCodeController {
+
+    //private final Notifier notifier;
+
+    private final NotifierManager notifierManager;
+
+    private final ReactiveRedisOperations<String, String> redis;
+
+    private final VerificationCodeProperties properties;
+
+    private final ApplicationEventPublisher eventPublisher;
+
+    /**
+     * @return token
+     */
+    @PostMapping("/send/{receiver}")
+    @Operation(summary = "发送验证码")
+    public Mono<String> sendCode(@PathVariable String receiver) {
+        //生成验证码
+        String code = RandomUtil.getCode();
+        //生成token
+        String token = RandomUtil.getToken();
+        //发送验证码并缓存,返回token
+        return redis
+            .opsForValue()
+            .set("code:" + RandomUtil.getCacheKey(receiver, token),
+                 code,
+                 Duration.ofSeconds(properties.getTermOfValidity()))
+            .then(properties
+                      .doSend(receiver, code)
+                      .thenReturn(token));
+    }
+
+    /**
+     * @return token
+     */
+    @PostMapping("/verify")
+    @Operation(summary = "验证验证码")
+    public Mono<String> verifyCode(@RequestBody CodeVerifyRequest request) {
+        ValidatorUtils.tryValidate(request);
+        VerifyCodeEvent verifyCodeEvent = new VerifyCodeEvent(request.getToken(),
+                                                              request.getCode(),
+                                                              request.getTelephone());
+        return verifyCodeEvent
+            .publish(eventPublisher)
+            .then(Mono.defer(() -> {
+                String token = RandomUtil.getCacheKey(request.getTelephone(), request.getToken());
+                return redis
+                    .opsForValue()
+                    .set("verify:" + RandomUtil.getCacheKey(request.getTelephone(), token),
+                         request.getTelephone(),
+                         Duration.ofSeconds(properties.getTermOfValidity()))
+                    .thenReturn(token);
+            }));
+
+    }
+
+    /**
+     * 校验验证码时发布该事件,此处订阅后等验证码进行验证
+     *
+     * @param event
+     */
+    @EventListener
+    public void handleVerifyCodeEvent(VerifyCodeEvent event) {
+        //获取缓存key
+        String cacheKey = "code:" + RandomUtil.getCacheKey(event.getReceiver(), event.getToken());
+        event.async(
+            redis
+                .opsForValue()
+                .get(cacheKey)
+                .switchIfEmpty(Mono.error(new BusinessException("验证码已失效")))
+                .map(code -> event.getCode().equals(code))
+                .defaultIfEmpty(false)
+                .flatMap(checked -> {
+                             if (checked) {
+                                 return redis
+                                     .delete(cacheKey)
+                                     .then();
+                             } else {
+                                 return Mono.error(new ValidationException("验证码错误"));
+                             }
+                         }
+                )
+        );
+    }
+
+
+    /**
+     * 校验验证码通过令牌时发布该事件,此处订阅后等验证码进行验证
+     *
+     * @param event
+     */
+    @EventListener
+    public void handleCodeValidatedEvent(CodeValidatedEvent event) {
+        //获取缓存key
+        String cacheKey = "verify:" + RandomUtil.getCacheKey(event.getTelephone(), event.getToken());
+        event.async(
+            redis
+                .opsForValue()
+                .get(cacheKey)
+                .map(username -> event.getUsername().equals(username))
+                .defaultIfEmpty(false)
+                .flatMap(checked -> {
+                             if (checked) {
+                                 return redis
+                                     .delete(cacheKey)
+                                     .then();
+                             } else {
+                                 return Mono.error(new ValidationException("验证失败"));
+                             }
+                         }
+                )
+        );
+    }
+}

+ 18 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeInfo.java

@@ -0,0 +1,18 @@
+package org.jetlinks.pro.cqfire.web.code;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Setter
+@Getter
+public class VerificationCodeInfo {
+
+    private String code;
+
+    private String token;
+}

+ 75 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/code/VerificationCodeProperties.java

@@ -0,0 +1,75 @@
+package org.jetlinks.pro.cqfire.web.code;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author wangzheng
+ * @since 1.10.0
+ */
+@Setter
+@Getter
+@ConfigurationProperties(prefix = "verify.code.notify")
+@Component
+@Slf4j
+public class VerificationCodeProperties {
+
+    /**
+     * 接口url
+     */
+    private String url;
+
+    /**
+     * 设置验证码有效时长
+     */
+    private Long termOfValidity;
+
+    /**
+     * 模板
+     */
+    private String template;
+
+
+    private WebClient webClient = WebClient.builder().build();
+
+
+    public Mono<Void> doSend(String receiver, String code) {
+        JSONObject body = new JSONObject();
+        body.put("phone", receiver);
+        body.put("content", template.replace("{code}", code));
+        body.put("remark", "物联网监测平台");
+        return webClient
+            .post()
+            .uri(url)
+            .bodyValue(body)
+            .retrieve()
+            .onStatus(HttpStatus::is4xxClientError, resp -> {
+                log.error("请求验证接口失败error:{},msg:{}", resp.statusCode().value(), resp
+                    .statusCode()
+                    .getReasonPhrase());
+                return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp
+                    .statusCode()
+                    .getReasonPhrase()));
+            })
+            .bodyToMono(String.class)
+            .flatMap(reply -> {
+                if (reply.contains("发送成功")) {
+                    return Mono.empty();
+                }
+                return Mono.error(new UnsupportedOperationException("短信接口返回失败:" + reply));
+            })
+            .then();
+
+
+    }
+
+}

+ 30 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/ChangeProcessStatusRequest.java

@@ -0,0 +1,30 @@
+package org.jetlinks.pro.cqfire.web.corp;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.pro.cqfire.enums.CorpProcessStatus;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+public class ChangeProcessStatusRequest {
+
+    @Schema(description = "企业ID")
+    @NotBlank(message = "企业ID不能为空")
+    private String corpId;
+
+    @Schema(description = "审核状态")
+    @NotNull(message = "审核状态不能为空")
+    private CorpProcessStatus status;
+
+    @Schema(description = "说明")
+    private String describe;
+}

+ 27 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/ChangeStatusRequest.java

@@ -0,0 +1,27 @@
+package org.jetlinks.pro.cqfire.web.corp;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.pro.cqfire.enums.CorpStatus;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Getter
+@Setter
+public class ChangeStatusRequest {
+
+    @Schema(description = "企业ID")
+    @NotBlank(message = "企业ID不能为空")
+    private String corpId;
+
+    @Schema(description = "企业状态")
+    @NotNull(message = "企业状态不能为空")
+    private CorpStatus status;
+}

+ 117 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/corp/CorpController.java

@@ -0,0 +1,117 @@
+package org.jetlinks.pro.cqfire.web.corp;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.annotation.*;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.pro.cqfire.entity.CorpEntity;
+import org.jetlinks.pro.cqfire.enums.CorpProcessStatus;
+import org.jetlinks.pro.cqfire.enums.CorpStatus;
+import org.jetlinks.pro.cqfire.service.corp.CorpService;
+import org.jetlinks.pro.cqfire.web.user.CorpUserDetail;
+import org.jetlinks.pro.tenant.annotation.TenantAssets;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Date;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@RestController
+@RequestMapping({"/corp"})
+@Authorize
+@Resource(id = "corp", name = "企业管理")
+@Slf4j
+@Tag(name = "企业管理")
+public class CorpController implements ReactiveServiceCrudController<CorpEntity, String> {
+    private final CorpService corpService;
+
+    public CorpController(CorpService corpService) {
+        this.corpService = corpService;
+    }
+
+    @Override
+    public ReactiveCrudService<CorpEntity, String> getService() {
+        return corpService;
+    }
+
+    @GetMapping("/list/_query")
+    @QueryAction
+    @Operation(summary = "获取通过审核(启用)的厂商id/名称/编码列表")
+    @TenantAssets(ignore = true)
+    public Flux<CorpEntity> getCorpInfo() {
+        QueryParamEntity queryParamEntity = new QueryParamEntity()
+            .and("status", "eq", CorpStatus.enabled)
+            .and("processStatus", "eq", CorpProcessStatus.passed)
+            .includes("id", "name", "code");
+        return corpService.query(queryParamEntity);
+    }
+
+
+    /**
+     * 分页查询企业详情
+     *
+     * @param param 查询条件
+     * @return 查询结果
+     */
+    @GetMapping("/details")
+    @QueryAction
+    @Operation(summary = "分页查询企业详情")
+    public Mono<PagerResult<CorpUserDetail>> queryPagingDetails(@Parameter(hidden = true) QueryParamEntity param) {
+        return corpService.queryCorpUserDetails(param);
+    }
+
+    /**
+     * 修改审核状态
+     *
+     * @return 是否成功
+     */
+    @PostMapping("/process-status")
+    @ResourceAction(id = "audit", name = "审核")
+    @Operation(summary = "修改审核状态")
+    public Mono<Boolean> changeProcessStatus(@RequestBody @Validated ChangeProcessStatusRequest request) {
+
+        return corpService.changeProcessStatus(request);
+    }
+
+    /**
+     * 提交审核资料
+     *
+     * @return 企业信息
+     */
+    @PostMapping("/commit")
+    @SaveAction
+    @Operation(summary = "提交审核资料")
+    public Mono<CorpEntity> commit(@RequestBody Mono<CorpEntity> entityMono) {
+
+        return Authentication
+            .currentReactive()
+            .flatMap(autz -> entityMono.map(entity -> applyAuthentication(entity, autz)))
+            .switchIfEmpty(entityMono)
+            .flatMap(corpService::commit);
+    }
+
+    /**
+     * 修改企业状态
+     *
+     * @return 是否成功
+     */
+    @PostMapping("/status")
+    @ResourceAction(id = "enable", name = "修改企业状态")
+    @Operation(summary = "修改企业状态")
+    public Mono<Boolean> restPassword(@RequestBody @Validated ChangeStatusRequest request) {
+
+        return corpService.changeStatus(request);
+    }
+}

+ 48 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/item/CategoryTestItemController.java

@@ -0,0 +1,48 @@
+package org.jetlinks.pro.cqfire.web.item;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.pro.cqfire.entity.CategoryTestItemEntity;
+import org.jetlinks.pro.cqfire.service.item.CategoryTestItemService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+
+@RestController
+@RequestMapping("/category/test/item")
+@AllArgsConstructor
+@Tag(name = "产品分类对应测试项")
+@Resource(id = "category-test-item", name = "产品分类对应测试项")
+public class CategoryTestItemController implements ReactiveServiceCrudController<CategoryTestItemEntity, String> {
+
+    private final CategoryTestItemService categoryTestItemService;
+
+    @Override
+    public ReactiveCrudService<CategoryTestItemEntity, String> getService() {
+        return categoryTestItemService;
+    }
+
+
+    @GetMapping("/{categoryId}/_query")
+    @SaveAction
+    @Operation(summary = "查询产品分类对应测试项")
+    public Flux<CategoryTestItemEntity> updateTestItem(@PathVariable String categoryId) {
+        return categoryTestItemService.getCategoryItems(categoryId);
+    }
+
+
+    @PostMapping("/{categoryId}/_update")
+    @SaveAction
+    @Operation(summary = "保存产品分类对应测试项")
+    public Flux<CategoryTestItemEntity> updateByCategory(@PathVariable String categoryId,
+                                                         @RequestBody @Validated Flux<TestItemRequest> request) {
+        return categoryTestItemService.updateByCategory(categoryId, request);
+    }
+
+
+}

+ 57 - 0
cq-fire/src/main/java/org/jetlinks/pro/cqfire/web/item/TestItemController.java

@@ -0,0 +1,57 @@
+package org.jetlinks.pro.cqfire.web.item;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.SaveAction;
+import org.hswebframework.web.authorization.exception.UnAuthorizedException;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.pro.cqfire.entity.ReportEntity;
+import org.jetlinks.pro.cqfire.entity.TestItemEntity;
+import org.jetlinks.pro.cqfire.service.item.TestItemService;
+import org.jetlinks.pro.cqfire.service.report.ReportService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/test/item")
+@AllArgsConstructor
+@Tag(name = "设备测试项")
+@Resource(id = "test-item", name = "设备测试项")
+public class TestItemController implements ReactiveServiceCrudController<TestItemEntity, String> {
+
+    private final TestItemService testItemService;
+    private final ReportService reportService;
+
+    @Override
+    public ReactiveCrudService<TestItemEntity, String> getService() {
+        return testItemService;
+    }
+
+    /**
+     * 增删改设备测试项
+     *
+     * @param deviceTestId 测试设备ID
+     * @param request      请求参数
+     * @return 返回结果
+     */
+    @PostMapping("/{deviceTestId}/_update")
+    @SaveAction
+    @Operation(summary = "增删改设备测试项")
+    public Flux<TestItemEntity> updateTestItem(@PathVariable String deviceTestId, @RequestBody @Validated Flux<TestItemRequest> request) {
+        return Authentication
+            .currentReactive()
+            .switchIfEmpty(Mono.error(UnAuthorizedException::new))
+            .flatMapMany(auth -> reportService
+                .createDelete()
+                .where(ReportEntity::getDeviceId, deviceTestId)
+                .execute()
+                .flatMapMany(count -> testItemService.addTestItems(deviceTestId, auth, request)));
+    }
+
+}

部分文件因文件數量過多而無法顯示