diff --git a/docs/DCP接口文档(轴力计).pdf b/docs/DCP接口文档(轴力计).pdf new file mode 100644 index 00000000..abc9a2bc Binary files /dev/null and b/docs/DCP接口文档(轴力计).pdf differ diff --git a/docs/knowledge-base/01-architecture.md b/docs/knowledge-base/01-architecture.md new file mode 100644 index 00000000..7465bdeb --- /dev/null +++ b/docs/knowledge-base/01-architecture.md @@ -0,0 +1,95 @@ +# 整体架构 + +## 项目定位 + +FastBee 是基于若依 v3.8.5 改造的物联网平台,覆盖设备接入、产品/物模型管理、数据采集、规则引擎、通知、视频监控、SCADA/GoView 可视化和移动端。 + +## 技术栈 + +| 层 | 技术 | +| --- | --- | +| 后端框架 | Spring Boot 2.5.14, Spring Security, JWT | +| ORM/数据访问 | MyBatis Plus, MyBatis XML Mapper, PageHelper | +| 数据源 | MySQL 主库,动态数据源,可选 TDengine | +| 缓存/锁 | Redis, Redisson, Lock4j | +| 协议接入 | Netty MQTT/TCP/UDP/HTTP/CoAP, SIP GB28181 | +| 规则引擎 | LiteFlow, Groovy 脚本 | +| 消息通道 | 本地 `LinkedBlockingQueue` 队列族,MQTT/EMQX 或内嵌 Broker | +| 前端 | Vue 2 + Element UI, Vue 3 + Vite + Naive UI, UniApp + uView | + +## 后端模块分层 + +```text +fastbee-admin + -> fastbee-framework + -> fastbee-open-api + -> fastbee-server/boot-strap + -> fastbee-service/* + -> fastbee-iot-data + -> fastbee-mq + -> fastbee-notify/* + -> fastbee-plugs/* + -> fastbee-common +``` + +| 模块 | 职责 | +| --- | --- | +| `fastbee-admin` | Spring Boot 主入口,端口默认 8080,聚合 Web/API/协议/插件依赖。 | +| `fastbee-framework` | 若依框架层:安全、鉴权、Web 配置、数据源、拦截器、通用基础设施。 | +| `fastbee-common` | 公共 DTO、枚举、工具、注解、Redis Key、MQ 消息模型、协议通用模型。 | +| `fastbee-open-api` | 对外 REST Controller,覆盖设备、产品、物模型、规则、视频、通知等业务 API。 | +| `fastbee-service/fastbee-system-service` | 系统管理服务:用户、角色、菜单、部门、字典、参数、日志。 | +| `fastbee-service/fastbee-iot-service` | IoT 核心业务:产品、设备、物模型、场景、规则、告警、Modbus、网关、固件。 | +| `fastbee-server` | Netty/SIP 协议服务集合:基础 Server、MQTT Broker、TCP/UDP/HTTP/CoAP/SIP、启动装配。 | +| `fastbee-protocol` | 协议抽象和具体编解码实现,如 Modbus、JSON、自定义采集协议、OTA 包。 | +| `fastbee-iot-data` | 设备数据处理层:上报处理、下发处理、规则触发、TDengine 日志、OTA、设备消息发布。 | +| `fastbee-mq` | 设备消息通道封装,当前核心是 JVM 内本地阻塞队列和消息生产入口。 | +| `fastbee-notify` | 通知配置、模板、日志和发送通道实现,包含邮件、短信、微信、钉钉等。 | +| `fastbee-plugs` | 插件能力:Quartz、代码生成、OSS、Forest HTTP、OAuth、MQTT Client、LiteFlow 扩展、支付。 | +| `fastbee-record` | 独立视频录像/回调服务,入口 `ZlmRecordApplication`,与 ZLMediaKit 配合使用。 | + +## 启动入口与条件装配 + +- 主应用入口:`fastbee-admin/src/main/java/com/fastbee/FastBeeApplication.java`。 +- `@SpringBootApplication` 扫描 `com.fastbee` 包下所有模块 Bean。 +- `@ForestScan(basePackages = "com.fastbee")` 扫描 Forest HTTP 客户端。 +- 协议服务由 `fastbee-server/boot-strap` 按配置条件创建 Netty `Server` Bean: + - `server.broker.enabled=true`:启动内嵌 MQTT Broker 和 WebSocket MQTT。 + - `server.tcp.enabled=true`:启动 TCP 服务,默认 8888。 + - `server.http.enabled=true`:启动 HTTP 设备接入服务,默认 8081。 + - `server.coap.enabled=true`:启动 CoAP 服务,默认 5683。 + - `sip.enabled=true`:启动 GB28181 视频接入。 + +## 前端组成 + +| 应用 | 目录 | 技术 | 角色 | +| --- | --- | --- | --- | +| 管理后台 | `yack-web` | Vue 2, Element UI, Vuex | 运维/管理端,覆盖系统、设备、产品、规则、告警等后台功能。 | +| 低代码可视化 | `yack-view` | Vue 3, TypeScript, Vite, Naive UI, Pinia | GoView 风格大屏和项目可视化编辑器。 | +| 移动端 | `yack-app` | UniApp, uView 2.0, MQTT.js | App/小程序/H5 设备查看、控制和移动操作。 | + +## 配置中心 + +| 文件 | 重点 | +| --- | --- | +| `fastbee-admin/src/main/resources/application.yml` | 端口、协议服务开关、JWT、MyBatis Plus、Redisson、Forest、LiteFlow SQL 规则源。 | +| `fastbee-admin/src/main/resources/application-dev.yml` | 本地 MySQL、Redis、MQTT、EMQX、SIP、Swagger 配置。 | +| `fastbee-admin/src/main/resources/application-prod.yml` | 生产环境覆盖配置。 | +| `fastbee-admin/src/main/resources/application-sql.yml` | SQL 相关配置。 | + +## 数据与依赖边界 + +```text +Controller(open-api/admin/plugs) + -> Service(fastbee-iot-service/system-service/notify/sip/oss) + -> Mapper XML + -> MySQL / TDengine + +Protocol Server(Netty/SIP/MQTT) + -> fastbee-mq queue + -> fastbee-iot-data handler + -> IoT Service + Rule Engine + Log Service + -> MQTT/WebSocket/DB/Notify +``` + +`fastbee-common` 不应反向依赖业务模块;`fastbee-framework` 提供框架装配;业务 Controller 优先调用 Service 接口,不直接写复杂 SQL。 diff --git a/docs/knowledge-base/02-runtime-flows.md b/docs/knowledge-base/02-runtime-flows.md new file mode 100644 index 00000000..92af861b --- /dev/null +++ b/docs/knowledge-base/02-runtime-flows.md @@ -0,0 +1,127 @@ +# 核心运行链路 + +## 应用启动 + +1. `FastBeeApplication` 启动 Spring Boot。 +2. `application.yml` 激活 profile,默认是 `prod`,本地开发通常改为 `dev`。 +3. 动态数据源初始化 `master`,可选初始化 `taos`。 +4. MyBatis Plus 扫描 `com.fastbee.**.domain` 和 `classpath*:mapper/**/*Mapper.xml`。 +5. `boot-strap` 模块按配置启动协议服务。 +6. LiteFlow 从数据库加载规则链和脚本: + - 规则链表:`iot_scene` + - 脚本表:`iot_script` + - 应用名字段:`application_name` + +## 设备上报链路 + +```text +设备 + -> MQTT/TCP/HTTP/CoAP/SIP 协议接入 + -> 协议解码 / Topic 解析 / Session 管理 + -> MessageProducer.sendPublishMsg 或其他队列入口 + -> fastbee-mq 本地队列 + -> fastbee-iot-data DataHandlerImpl + -> 设备物模型值更新 + -> 设备日志/事件日志/TDengine + -> 规则引擎/场景联动/条件触发 + -> WebSocket/MQTT 推送到前端 +``` + +关键代码位置: + +| 环节 | 位置 | +| --- | --- | +| MQTT Broker 启动 | `fastbee-server/boot-strap/.../MQTTBootStrap.java` | +| TCP 服务启动 | `fastbee-server/boot-strap/.../TCPBootStrap.java` | +| 消息生产入口 | `fastbee-mq/.../MessageProducer.java` | +| 上报队列 | `fastbee-mq/.../DeviceReportQueue.java` | +| 上报处理 | `fastbee-iot-data/.../DataHandlerImpl.java` | +| 物模型更新 | `fastbee-service/fastbee-iot-service` 下 `IDeviceService` 实现 | +| 规则匹配 | `fastbee-iot-data` 的 `IRuleEngine` 和 `fastbee-iot-service` 的 `RuleProcess` | + +## 服务/指令下发链路 + +```text +前端/API + -> DeviceMessageServiceImpl.messagePost / MqttMessagePublishImpl.funcSend + -> 查询设备产品、协议、物模型和传输方式 + -> MQTT: 发布到设备 Topic + -> TCP: 组包后通过 SessionManager/MessageManager 写回连接 + -> GB28181: 调用 SIP Gateway + -> FunctionLog 记录下发结果 + -> WebSocket/MQTT 推送调试消息到前端 +``` + +下发时的重要分支: + +| 分支 | 处理 | +| --- | --- | +| MQTT 设备 | 使用 `PubMqttClient.publish` 发布到 `TopicsUtils.buildTopic(...)` 生成的 Topic。 | +| TCP 设备 | 将指令转为协议字节,交给 `MessageManager.requestR` 对对应 Session 写出。 | +| 子设备 | 先解析网关设备关系,把子设备指令映射到网关产品/设备,再设置 slave 地址。 | +| 影子模式 | 不真实下发,直接构造上报数据并走 `dataHandler.reportData` 更新影子值。 | +| 离线延迟下发 | `deferIfOffline=true` 时写入 `iot_device_deferred_command`,等待设备上线后补发。 | + +## 规则引擎链路 + +规则体系有两层: + +| 类型 | 入口 | 数据表 | +| --- | --- | --- | +| 场景联动 | `IRuleEngine.ruleMatch(...)` | `iot_scene`, `iot_scene_device`, `iot_scene_script`, `iot_device_job` | +| 数据桥接/脚本 | `RuleProcess.processRuleScript(...)` | `iot_script`, `bridge`, `iot_script_bridge` | + +LiteFlow SQL 规则源配置在 `application.yml`: + +```text +chainTableName: iot_scene +scriptTableName: iot_script +chainEnableField: enable +scriptEnableField: enable +``` + +设备 TCP 属性上报后,如果启用规则引擎,会额外以 `tcp/property/post` 作为桥接 Topic 调用 `RuleProcess`,用于 HTTP/MQTT/数据库桥接。 + +## 数据持久化链路 + +| 数据 | 主要表 | +| --- | --- | +| 产品、分类、协议 | `iot_product`, `iot_category`, `iot_protocol` | +| 设备与绑定 | `iot_device`, `iot_device_user`, `iot_device_group`, `iot_sub_gateway` | +| 物模型 | `iot_things_model`, `iot_things_model_template`, 翻译表 | +| 上报日志 | `iot_device_log`, `iot_event_log`, `iot_function_log`, `iot_alert_log` | +| 固件升级 | `iot_firmware`, `iot_firmware_task`, `iot_firmware_task_detail` | +| 规则/桥接 | `iot_scene`, `iot_script`, `bridge`, `iot_script_bridge` | +| 通知 | `notify_channel`, `notify_template`, `notify_log` | +| 视频 | `sip_device`, `sip_device_channel`, `media_server`, `iot_sip_relation` | + +TDengine 可选数据源 `taos` 默认禁用,相关日志服务在 `fastbee-iot-service` 的 `tdengine` 包和 `fastbee-iot-data` 中被调用。 + +## 视频监控链路 + +```text +GB28181 设备/NVR + -> SIP Server + -> sip_device / sip_device_channel + -> ZLMediaKit media_server + -> 播放、PTZ、录像、回放 API + -> fastbee-record 可独立承接录像相关回调/下载 +``` + +视频相关 Controller 分布在 `fastbee-open-api/src/main/java/com/fastbee/controller/media` 和 `fastbee-record`。 + +## 通知链路 + +```text +业务事件/告警/推送配置 + -> notify_template + -> notify_channel + -> fastbee-notify-core 具体渠道 + -> notify_log +``` + +物模型推送扩展表: + +- `iot_things_model_push_config`:某设备的物模型推送配置。 +- `iot_push_model_item`:配置下需要推送的物模型项和阈值。 +- `iot_push_user`:配置下的接收用户。 diff --git a/docs/knowledge-base/03-data-model.md b/docs/knowledge-base/03-data-model.md new file mode 100644 index 00000000..b06d94e4 --- /dev/null +++ b/docs/knowledge-base/03-data-model.md @@ -0,0 +1,280 @@ +# 数据库与表关系 + +## 数据库来源 + +主初始化脚本:`sql/yack-20260316.sql`。 + +补充脚本: + +- `sql/iot_device_deferred_command.sql` +- `sql/iot_things_model_push.sql` +- `sql/yack.sql` + +`yack-20260316.sql` 中包含约 100+ 张核心表,并带有大量初始化数据。业务表普遍不使用数据库外键;关系主要由字段命名、联合主键、索引和业务代码保证。Quartz 表保留真实外键。 + +## 通用字段约定 + +| 字段 | 含义 | +| --- | --- | +| `tenant_id`, `tenant_name` | 多租户归属,IoT/通知/可视化等表常见。 | +| `user_id`, `user_name` | 用户归属或操作者。 | +| `create_by`, `create_time`, `update_by`, `update_time` | 若依通用审计字段。 | +| `del_flag` | 软删除标记,常见取值 0 正常、2 删除。 | +| `status` | 业务状态,不同表含义不同,需结合枚举/字典。 | +| `serial_number` | 设备唯一编号,是设备日志、Topic、指令和网关关系中的高频关联键。 | + +## 核心业务 ER 概览 + +```mermaid +erDiagram + SYS_USER ||--o{ IOT_DEVICE_USER : owns + SYS_USER ||--o{ IOT_GROUP : creates + SYS_USER ||--o{ IOT_DEVICE_SHARE : shared_to + IOT_CATEGORY ||--o{ IOT_PRODUCT : classifies + IOT_PRODUCT ||--o{ IOT_DEVICE : has + IOT_PRODUCT ||--o{ IOT_THINGS_MODEL : defines + IOT_DEVICE ||--o{ IOT_THINGS_MODEL : overrides + IOT_DEVICE ||--o{ IOT_DEVICE_LOG : reports + IOT_DEVICE ||--o{ IOT_EVENT_LOG : emits + IOT_DEVICE ||--o{ IOT_FUNCTION_LOG : receives + IOT_PRODUCT ||--o{ IOT_FIRMWARE : owns + IOT_FIRMWARE ||--o{ IOT_FIRMWARE_TASK : upgrades + IOT_FIRMWARE_TASK ||--o{ IOT_FIRMWARE_TASK_DETAIL : tracks + IOT_SCENE ||--o{ IOT_SCENE_DEVICE : triggers + IOT_SCENE ||--o{ IOT_SCENE_SCRIPT : actions + IOT_SCRIPT ||--o{ IOT_SCRIPT_BRIDGE : uses + BRIDGE ||--o{ IOT_SCRIPT_BRIDGE : bound +``` + +上图是业务约定关系,不代表 SQL 外键。 + +## 产品、设备、物模型 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_category` | `category_id` | 产品分类,支持 `parent_id` 层级。 | `iot_product.category_id` 关联分类。 | +| `iot_product` | `product_id` | 产品定义,包含分类、租户、协议、设备类型等。 | 一对多关联设备、物模型、固件、Modbus 配置。 | +| `iot_protocol` | `id` | 协议定义,`protocol_code` 唯一。 | 产品通过协议编码/配置选择编解码实现。 | +| `iot_device` | `device_id` | 设备注册表,`serial_number` 唯一。 | 关联产品、租户、用户、日志、网关、物模型覆盖。 | +| `iot_things_model` | `model_id` | 产品级/设备级物模型。 | `product_id` 关联产品;`device_id=0` 通常表示产品级,`device_id>0` 表示设备级覆盖或计算物模型。 | +| `iot_things_model_template` | `template_id` | 物模型模板。 | 可用于产品物模型复用。 | +| `iot_device_template` | `id` | 产品和采集点模板关系。 | `product_id` + `template_id`。 | +| `iot_things_model_translate` | `id` | 物模型翻译。 | 通过 `product_id`、标识符等业务字段关联。 | +| `iot_things_model_template_translate` | `id` | 物模型模板翻译。 | 通过模板标识业务关联。 | + +关键约束/索引: + +- `iot_device.serial_number` 是唯一索引。 +- `iot_product.category_id`、`iot_product.tenant_id` 有普通索引。 +- `iot_things_model.product_id`、`tenant_id`、`model_order` 有普通索引。 +- `iot_things_model` 还有 `idx_calculated_serial(is_calculated, serial_number)`,用于计算物模型按设备检索。 + +## 设备归属、共享和分组 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_device_user` | `device_id`, `user_id` | 设备与用户归属关系。 | 关联 `iot_device` 和 `sys_user`。 | +| `iot_device_share` | `device_id`, `user_id` | 设备共享关系和权限。 | 关联设备与被共享用户。 | +| `iot_device_group` | `device_id`, `group_id` | 设备加入分组。 | 关联 `iot_device` 和 `iot_group`。 | +| `iot_group` | `group_id` | 用户创建的设备分组。 | `user_id` 关联 `sys_user`。 | +| `iot_device_alert_user` | `device_id`, `user_id` | 设备告警接收用户。 | 关联设备与告警接收用户。 | +| `order_control` | `id` | 指令权限控制。 | `user_id` + `device_id` 限制控制权限。 | +| `command_preferences` | `id` | 指令偏好模板。 | `product_id` 和 `serial_number` 关联设备/产品。 | + +## 网关、子设备和 Modbus + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_sub_gateway` | `id` | 网关设备与子设备关系。 | `gw_device_id`、`sub_device_id` 均约定关联 `iot_device.device_id`。 | +| `iot_product_sub_gateway` | `id` | 网关产品与子产品关系。 | `gw_product_id`、`sub_product_id` 约定关联 `iot_product.product_id`。 | +| `iot_modbus_config` | `id` | 产品物模型的 Modbus 点位配置。 | `product_id` 关联产品。 | +| `iot_modbus_params` | `id` | 产品级 Modbus 默认参数。 | `product_id` 关联产品,`slave_id` 表示默认从机。 | +| `iot_modbus_job` | `task_id` | 设备轮询任务。 | `sub_device_id`、`job_id` 是业务关联字段。 | +| `iot_product_modbus_job` | `task_id` | 产品轮询任务模板。 | `product_id` 关联产品。 | +| `iot_var_temp` | `template_id` | 采集变量模板。 | `user_id` 关联模板所属用户。 | +| `iot_var_temp_salve` | `id` | 模板下从机/点位配置。 | `device_temp_id` 关联 `iot_var_temp.template_id`。 | + +## 数据日志 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_device_log` | `log_id` | 设备属性/监测日志。 | `device_id`、`serial_number`、`tenant_id`、`user_id`。 | +| `iot_event_log` | `log_id` | 设备事件日志。 | 关联设备、租户和用户。 | +| `iot_function_log` | `id` | 服务/指令下发日志。 | `serial_number`、`message_id`、`user_id`。 | +| `iot_alert_log` | `alert_log_id` | 告警历史。 | `serial_number`、`product_id`、`user_id`。 | +| `iot_simulate_log` | `log_id` | 模拟设备日志。 | `device_id`、`serial_number`。 | +| `iot_device_air_log` | `id` | Air780 上报记录。 | `product_id`、`device_id`。 | + +高频查询索引: + +- `iot_device_log(serial_number, create_time)` +- `iot_device_log(serial_number, is_monitor, create_time)` +- `iot_alert_log(serial_number)` +- `iot_alert_log(product_id)` +- `iot_device_log(device_id)` + +## 规则、场景、桥接和告警 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_scene` | `scene_id` | 场景联动/LiteFlow 规则链。 | `user_id`,`recover_id` 可指向恢复场景。 | +| `iot_scene_device` | `scene_device_id` | 场景触发设备/产品。 | `scene_id`、`product_id`、`serial_number`、`script_id`。 | +| `iot_scene_script` | `script_id` | 场景动作/条件脚本。 | `scene_id`、`product_id`、`job_id`、`parent_id`。 | +| `iot_script` | `script_id` | 规则引擎脚本组件。 | `scene_id`、`product_id`、`user_id`。 | +| `bridge` | `id` | 数据桥接端点配置。 | HTTP/MQTT/数据库桥接配置主体。 | +| `iot_script_bridge` | `script_id`, `bridge_id` | 脚本与桥接端点关系。 | 多对多关联 `iot_script` 和 `bridge`。 | +| `iot_alert` | `alert_id` | 告警定义。 | 租户维度定义告警。 | +| `iot_alert_scene` | `alert_id`, `scene_id` | 告警与场景关系。 | 多对多关联告警和场景。 | +| `iot_alert_notify_template` | `alert_id`, `notify_template_id` | 告警与通知模板关系。 | 多对多关联告警和通知模板。 | +| `iot_device_job` | `job_id`, `job_name`, `job_group` | 设备定时任务。 | 可关联设备、产品、场景、告警。 | + +LiteFlow SQL 源直接读取: + +- `iot_scene.chain_name` +- `iot_scene.el_data` +- `iot_scene.enable` +- `iot_script.script_id` +- `iot_script.script_data` +- `iot_script.script_language` +- `iot_script.enable` + +## 场景模型、SCADA 和可视化 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `scene_model` | `scene_model_id` | 业务场景管理。 | 租户维度。 | +| `scene_model_device` | `id` | 场景关联设备/虚拟数据源。 | `scene_model_id`、`cus_device_id`。 | +| `scene_model_data` | `id` | 场景数据源绑定。 | `scene_model_id`、`scene_model_device_id`、`datasource_id`。 | +| `scene_model_tag` | `id` | 场景录入型变量/计算变量。 | `scene_model_id`。 | +| `scene_tag_points` | `id` | 运算型变量点。 | `tag_id` 关联变量,`scene_model_data_id` 关联数据源。 | +| `iot_goview_project` | `id` | GoView 项目。 | 租户维度。 | +| `iot_goview_project_data` | `id` | GoView 项目数据。 | `project_id` 关联项目。 | +| `scada` | `id` | 组态页面。 | 租户维度。 | +| `scada_component` | `id` | 组态组件。 | `tenant_id`、`user_id`。 | +| `scada_device_bind` | `id` | 组态组件设备绑定。 | `serial_number` 关联设备。 | +| `scada_echart` | `id` | 图表管理。 | 租户维度。 | +| `scada_gallery` | `id` | 组态图库。 | 租户维度。 | +| `scada_model` | `id` | 三维模型配置。 | 租户维度。 | + +## 固件和 OTA + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `iot_firmware` | `firmware_id` | 产品固件版本。 | `product_id` 关联产品,`tenant_id` 关联租户。 | +| `iot_firmware_task` | `id` | 固件升级任务。 | `firmware_id` 关联固件。 | +| `iot_firmware_task_detail` | `id` | 任务下设备升级状态。 | `task_id` 关联任务,`serial_number` 关联设备。 | + +## 视频监控和媒体 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `sip_config` | `id` | SIP 系统配置。 | 可关联 `product_id`。 | +| `sip_device` | `device_id` | GB28181 监控设备/NVR。 | `product_id`,`device_sip_id`。 | +| `sip_device_channel` | `id`, `device_sip_id` | 摄像头通道。 | `device_sip_id` 关联 SIP 设备编号,`channel_sip_id` 是通道编号。 | +| `media_server` | `id` | ZLMediaKit 流媒体服务器配置。 | `server_id` 是媒体服务标识。 | +| `iot_sip_relation` | `id` | IoT 设备/场景与 SIP 通道关系。 | `channel_id`、`re_device_id`、`re_scene_model_id`。 | + +## 通知和推送 + +| 表 | 主键 | 说明 | 关键关系 | +| --- | --- | --- | --- | +| `notify_channel` | `id` | 通知渠道账号。 | 租户维度。 | +| `notify_template` | `id` | 通知模板。 | `channel_id` 关联通知渠道。 | +| `notify_log` | `id` | 通知发送日志。 | `channel_id`、`notify_template_id`。 | +| `iot_things_model_push_config` | `id` | 物模型数据推送配置。 | `device_id`、`template_id`、`user_id`。 | +| `iot_push_model_item` | `id` | 推送配置下的物模型项和阈值。 | `config_id` 关联推送配置,`model_id` 关联物模型。 | +| `iot_push_user` | `id` | 推送接收用户。 | `config_id` 关联推送配置,`user_id` 关联系统用户。 | + +## 系统 RBAC 和基础表 + +```mermaid +erDiagram + SYS_DEPT ||--o{ SYS_USER : contains + SYS_USER ||--o{ SYS_USER_ROLE : has + SYS_ROLE ||--o{ SYS_USER_ROLE : assigned + SYS_ROLE ||--o{ SYS_ROLE_MENU : grants + SYS_MENU ||--o{ SYS_ROLE_MENU : included + SYS_ROLE ||--o{ SYS_ROLE_DEPT : scopes + SYS_DEPT ||--o{ SYS_ROLE_DEPT : scoped + SYS_USER ||--o{ SYS_USER_POST : holds + SYS_POST ||--o{ SYS_USER_POST : assigned +``` + +| 表 | 主键 | 说明 | +| --- | --- | --- | +| `sys_user` | `user_id` | 用户。 | +| `sys_dept` | `dept_id` | 部门/机构,`parent_id` 形成树。 | +| `sys_role` | `role_id` | 角色。 | +| `sys_menu` | `menu_id` | 菜单和权限,`parent_id` 形成树。 | +| `sys_post` | `post_id` | 岗位。 | +| `sys_user_role` | `user_id`, `role_id` | 用户角色关系。 | +| `sys_role_menu` | `role_id`, `menu_id` | 角色菜单权限。 | +| `sys_role_dept` | `role_id`, `dept_id` | 角色数据权限。 | +| `sys_user_post` | `user_id`, `post_id` | 用户岗位关系。 | +| `sys_config` | `config_id` | 系统参数。 | +| `sys_dict_type` | `dict_id` | 字典类型,`dict_type` 唯一。 | +| `sys_dict_data` | `dict_code` | 字典数据,通过 `dict_type` 关联字典类型。 | +| `sys_notice` | `notice_id` | 通知公告。 | +| `sys_oper_log` | `oper_id` | 操作日志。 | +| `sys_logininfor` | `info_id` | 登录日志。 | +| `sys_client` | `id` | 系统授权客户端。 | +| `sys_auth_user` | `auth_id` | 第三方授权用户。 | +| `sys_user_relation` | `root_id` | 用户关系和层级深度缓存。 | + +翻译表: + +- `sys_menu_translate` +- `sys_dict_type_translate` +- `sys_dict_data_translate` + +## OAuth、第三方登录和内容 + +| 表 | 说明 | +| --- | --- | +| `oauth_client_details` | OAuth 客户端配置。 | +| `oauth_access_token`, `oauth_refresh_token`, `oauth_client_token`, `oauth_code`, `oauth_approvals` | Spring OAuth Token/授权数据。 | +| `iot_social_platform` | 第三方登录平台配置。 | +| `iot_social_user` | 第三方用户和系统用户绑定。 | +| `app_language` | App 语言配置。 | +| `app_preferences` | 用户 App 偏好,`user_id + language` 唯一。 | +| `news`, `news_category` | 新闻资讯和分类。 | + +## OSS、代码生成和任务 + +| 表 | 说明 | +| --- | --- | +| `oss_config` | 对象存储配置。 | +| `oss_detail` | 上传文件记录。 | +| `gen_table`, `gen_table_column` | 代码生成器元数据。 | +| `sys_job`, `sys_job_log` | 若依定时任务配置和日志。 | + +## Quartz 强约束关系 + +这些是 SQL 中真实存在的外键: + +```text +qrtz_triggers(sched_name, job_name, job_group) + -> qrtz_job_details(sched_name, job_name, job_group) + +qrtz_blob_triggers(sched_name, trigger_name, trigger_group) + -> qrtz_triggers(sched_name, trigger_name, trigger_group) + +qrtz_cron_triggers(sched_name, trigger_name, trigger_group) + -> qrtz_triggers(sched_name, trigger_name, trigger_group) + +qrtz_simple_triggers(sched_name, trigger_name, trigger_group) + -> qrtz_triggers(sched_name, trigger_name, trigger_group) + +qrtz_simprop_triggers(sched_name, trigger_name, trigger_group) + -> qrtz_triggers(sched_name, trigger_name, trigger_group) +``` + +其他 Quartz 表如 `qrtz_calendars`、`qrtz_fired_triggers`、`qrtz_locks`、`qrtz_scheduler_state` 由 Quartz 框架维护。 + +## 关系梳理原则 + +- 优先看 SQL 主键、联合主键、唯一索引和普通索引。 +- 再看 Java Domain、Mapper XML、Service 查询条件。 +- 对无外键的业务表,文档只标记为“业务约定关联”。 +- `serial_number` 是很多运行时链路的核心关联键,但真正的设备主键仍是 `iot_device.device_id`。 +- `product_id`、`device_id`、`user_id`、`tenant_id` 等字段在不同表中含义一致,但删除/更新一致性由业务代码维护。 diff --git a/docs/knowledge-base/04-er-diagrams.md b/docs/knowledge-base/04-er-diagrams.md new file mode 100644 index 00000000..ffd96a37 --- /dev/null +++ b/docs/knowledge-base/04-er-diagrams.md @@ -0,0 +1,703 @@ +# 数据库关系图 + +本文件用 Mermaid ER 图描述 FastBee 数据库关系。除 Quartz 表外,大部分业务表没有 SQL 外键,图中的连线表示业务约定关联,主要依据字段命名、联合主键、索引和代码查询关系整理。 + +## 总览 + +```mermaid +flowchart LR + System["系统 RBAC
sys_*"] --> IoT["IoT 核心
产品/设备/物模型"] + IoT --> DataLog["数据日志
上报/事件/下发/告警日志"] + IoT --> Gateway["网关与 Modbus
子设备/轮询/点位"] + IoT --> Rule["规则与告警
场景/LiteFlow/桥接"] + IoT --> Firmware["固件 OTA
任务/明细"] + IoT --> Visual["可视化
SceneModel/SCADA/GoView"] + IoT --> Video["视频 GB28181
SIP/ZLM"] + Rule --> Notify["通知
渠道/模板/日志"] + System --> OAuth[OAuth/第三方登录] + System --> Gen[代码生成/OSS/Quartz] +``` + +## 产品、设备、物模型 + +```mermaid +erDiagram + IOT_CATEGORY ||--o{ IOT_CATEGORY : parent + IOT_CATEGORY ||--o{ IOT_PRODUCT : classifies + IOT_PRODUCT ||--o{ IOT_DEVICE : owns + IOT_PRODUCT ||--o{ IOT_THINGS_MODEL : defines + IOT_PRODUCT ||--o{ IOT_THINGS_MODEL_TEMPLATE : uses + IOT_PRODUCT ||--o{ IOT_DEVICE_TEMPLATE : maps_template + IOT_THINGS_MODEL_TEMPLATE ||--o{ IOT_DEVICE_TEMPLATE : assigned + IOT_THINGS_MODEL ||--o{ IOT_THINGS_MODEL_TRANSLATE : translated + IOT_THINGS_MODEL_TEMPLATE ||--o{ IOT_THINGS_MODEL_TEMPLATE_TRANSLATE : translated + IOT_PROTOCOL ||--o{ IOT_PRODUCT : selected_by + SYS_USER ||--o{ IOT_PRODUCT : tenant_or_owner + SYS_USER ||--o{ IOT_DEVICE : tenant_or_owner + + IOT_CATEGORY { + bigint category_id PK + bigint parent_id + bigint tenant_id + } + IOT_PRODUCT { + bigint product_id PK + bigint category_id + bigint tenant_id + } + IOT_DEVICE { + bigint device_id PK + bigint product_id + varchar serial_number UK + bigint tenant_id + bigint user_id + } + IOT_THINGS_MODEL { + bigint model_id PK + bigint product_id + bigint device_id + varchar serial_number + bigint tenant_id + } + IOT_THINGS_MODEL_TEMPLATE { + bigint template_id PK + bigint tenant_id + } + IOT_DEVICE_TEMPLATE { + bigint id PK + bigint product_id + bigint template_id + } + IOT_PROTOCOL { + bigint id PK + varchar protocol_code UK + } +``` + +## 设备归属、共享、分组 + +```mermaid +erDiagram + SYS_USER ||--o{ IOT_DEVICE_USER : owns + IOT_DEVICE ||--o{ IOT_DEVICE_USER : owned_by + SYS_USER ||--o{ IOT_DEVICE_SHARE : shared_to + IOT_DEVICE ||--o{ IOT_DEVICE_SHARE : shared + SYS_USER ||--o{ IOT_GROUP : creates + IOT_GROUP ||--o{ IOT_DEVICE_GROUP : contains + IOT_DEVICE ||--o{ IOT_DEVICE_GROUP : grouped + SYS_USER ||--o{ IOT_DEVICE_ALERT_USER : receives_alert + IOT_DEVICE ||--o{ IOT_DEVICE_ALERT_USER : alert_users + SYS_USER ||--o{ ORDER_CONTROL : restricted + IOT_DEVICE ||--o{ ORDER_CONTROL : command_acl + IOT_PRODUCT ||--o{ COMMAND_PREFERENCES : command_template + IOT_DEVICE ||--o{ COMMAND_PREFERENCES : device_command_template + + IOT_DEVICE_USER { + bigint device_id PK + bigint user_id PK + } + IOT_DEVICE_SHARE { + bigint device_id PK + bigint user_id PK + } + IOT_GROUP { + bigint group_id PK + bigint user_id + } + IOT_DEVICE_GROUP { + bigint device_id PK + bigint group_id PK + } + IOT_DEVICE_ALERT_USER { + bigint device_id PK + bigint user_id PK + } + ORDER_CONTROL { + bigint id PK + bigint user_id + bigint device_id + bigint tenant_id + } + COMMAND_PREFERENCES { + bigint id PK + bigint product_id + varchar serial_number + } +``` + +## 网关、子设备、Modbus + +```mermaid +erDiagram + IOT_DEVICE ||--o{ IOT_SUB_GATEWAY : gateway + IOT_DEVICE ||--o{ IOT_SUB_GATEWAY : sub_device + IOT_PRODUCT ||--o{ IOT_PRODUCT_SUB_GATEWAY : gateway_product + IOT_PRODUCT ||--o{ IOT_PRODUCT_SUB_GATEWAY : sub_product + IOT_PRODUCT ||--o{ IOT_MODBUS_CONFIG : point_config + IOT_PRODUCT ||--o{ IOT_MODBUS_PARAMS : params + IOT_PRODUCT ||--o{ IOT_PRODUCT_MODBUS_JOB : poll_template + IOT_DEVICE ||--o{ IOT_MODBUS_JOB : poll_task + IOT_VAR_TEMP ||--o{ IOT_VAR_TEMP_SALVE : slave_points + SYS_USER ||--o{ IOT_VAR_TEMP : owns + + IOT_SUB_GATEWAY { + bigint id PK + bigint gw_device_id + bigint sub_device_id + int slave_id + } + IOT_PRODUCT_SUB_GATEWAY { + bigint id PK + bigint gw_product_id + bigint sub_product_id + int slave_id + } + IOT_MODBUS_CONFIG { + bigint id PK + bigint product_id + } + IOT_MODBUS_PARAMS { + bigint id PK + bigint product_id + int slave_id + } + IOT_MODBUS_JOB { + bigint task_id PK + bigint sub_device_id + bigint job_id + } + IOT_PRODUCT_MODBUS_JOB { + bigint task_id PK + bigint product_id + } + IOT_VAR_TEMP { + bigint template_id PK + bigint user_id + } + IOT_VAR_TEMP_SALVE { + bigint id PK + bigint device_temp_id + } +``` + +## 设备数据日志 + +```mermaid +erDiagram + IOT_PRODUCT ||--o{ IOT_ALERT_LOG : product_alerts + IOT_DEVICE ||--o{ IOT_DEVICE_LOG : property_logs + IOT_DEVICE ||--o{ IOT_EVENT_LOG : event_logs + IOT_DEVICE ||--o{ IOT_FUNCTION_LOG : function_logs + IOT_DEVICE ||--o{ IOT_ALERT_LOG : alert_logs + IOT_DEVICE ||--o{ IOT_SIMULATE_LOG : simulate_logs + IOT_DEVICE ||--o{ IOT_DEVICE_AIR_LOG : air_logs + SYS_USER ||--o{ IOT_DEVICE_LOG : user_scope + SYS_USER ||--o{ IOT_EVENT_LOG : user_scope + SYS_USER ||--o{ IOT_FUNCTION_LOG : operator + + IOT_DEVICE_LOG { + bigint log_id PK + bigint device_id + varchar serial_number + bigint user_id + bigint tenant_id + datetime create_time + } + IOT_EVENT_LOG { + bigint log_id PK + bigint device_id + varchar serial_number + bigint user_id + bigint tenant_id + } + IOT_FUNCTION_LOG { + bigint id PK + varchar message_id + varchar serial_number + bigint user_id + } + IOT_ALERT_LOG { + bigint alert_log_id PK + varchar serial_number + bigint product_id + bigint user_id + } + IOT_SIMULATE_LOG { + bigint log_id PK + bigint device_id + varchar serial_number + } + IOT_DEVICE_AIR_LOG { + bigint id PK + bigint product_id + bigint device_id + } +``` + +## 规则、场景、告警、桥接 + +```mermaid +erDiagram + SYS_USER ||--o{ IOT_SCENE : owns + IOT_SCENE ||--o{ IOT_SCENE_DEVICE : has_trigger + IOT_SCENE ||--o{ IOT_SCENE_SCRIPT : has_action + IOT_PRODUCT ||--o{ IOT_SCENE_DEVICE : trigger_product + IOT_PRODUCT ||--o{ IOT_SCENE_SCRIPT : action_product + IOT_SCRIPT ||--o{ IOT_SCRIPT_BRIDGE : uses + BRIDGE ||--o{ IOT_SCRIPT_BRIDGE : endpoint + IOT_SCENE ||--o{ IOT_SCRIPT : liteflow_script + IOT_ALERT ||--o{ IOT_ALERT_SCENE : triggers_scene + IOT_SCENE ||--o{ IOT_ALERT_SCENE : alert_bound + IOT_ALERT ||--o{ IOT_ALERT_NOTIFY_TEMPLATE : notifies + NOTIFY_TEMPLATE ||--o{ IOT_ALERT_NOTIFY_TEMPLATE : used_by_alert + IOT_DEVICE ||--o{ IOT_DEVICE_JOB : scheduled + IOT_PRODUCT ||--o{ IOT_DEVICE_JOB : scheduled + IOT_SCENE ||--o{ IOT_DEVICE_JOB : scheduled + IOT_ALERT ||--o{ IOT_DEVICE_JOB : scheduled + + IOT_SCENE { + bigint scene_id PK + bigint user_id + varchar chain_name + text el_data + int enable + } + IOT_SCENE_DEVICE { + bigint scene_device_id PK + bigint scene_id + bigint product_id + varchar serial_number + bigint script_id + } + IOT_SCENE_SCRIPT { + bigint script_id PK + bigint scene_id + bigint product_id + bigint job_id + bigint parent_id + } + IOT_SCRIPT { + bigint script_id PK + bigint scene_id + bigint product_id + bigint user_id + int enable + } + BRIDGE { + bigint id PK + } + IOT_SCRIPT_BRIDGE { + bigint script_id PK + bigint bridge_id PK + } + IOT_ALERT { + bigint alert_id PK + bigint tenant_id + } + IOT_ALERT_SCENE { + bigint alert_id PK + bigint scene_id PK + } + IOT_ALERT_NOTIFY_TEMPLATE { + bigint alert_id PK + bigint notify_template_id PK + } + IOT_DEVICE_JOB { + bigint job_id PK + bigint device_id + bigint product_id + bigint scene_id + bigint alert_id + } +``` + +## 通知与物模型推送 + +```mermaid +erDiagram + NOTIFY_CHANNEL ||--o{ NOTIFY_TEMPLATE : has + NOTIFY_CHANNEL ||--o{ NOTIFY_LOG : sends + NOTIFY_TEMPLATE ||--o{ NOTIFY_LOG : produces + IOT_DEVICE ||--o{ IOT_THINGS_MODEL_PUSH_CONFIG : push_config + IOT_PRODUCT ||--o{ IOT_THINGS_MODEL_PUSH_CONFIG : product_scope + NOTIFY_TEMPLATE ||--o{ IOT_THINGS_MODEL_PUSH_CONFIG : template + SYS_USER ||--o{ IOT_THINGS_MODEL_PUSH_CONFIG : owner + IOT_THINGS_MODEL_PUSH_CONFIG ||--o{ IOT_PUSH_MODEL_ITEM : includes + IOT_THINGS_MODEL ||--o{ IOT_PUSH_MODEL_ITEM : model_item + IOT_THINGS_MODEL_PUSH_CONFIG ||--o{ IOT_PUSH_USER : receivers + SYS_USER ||--o{ IOT_PUSH_USER : receiver + + NOTIFY_CHANNEL { + bigint id PK + bigint tenant_id + } + NOTIFY_TEMPLATE { + bigint id PK + bigint channel_id + bigint tenant_id + } + NOTIFY_LOG { + bigint id PK + bigint channel_id + bigint notify_template_id + bigint tenant_id + } + IOT_THINGS_MODEL_PUSH_CONFIG { + bigint id PK + bigint product_id + bigint device_id + varchar serial_number + bigint template_id + bigint user_id + } + IOT_PUSH_MODEL_ITEM { + bigint id PK + bigint config_id + bigint model_id + } + IOT_PUSH_USER { + bigint id PK + bigint config_id + bigint user_id + } +``` + +## 固件 OTA + +```mermaid +erDiagram + IOT_PRODUCT ||--o{ IOT_FIRMWARE : owns + IOT_FIRMWARE ||--o{ IOT_FIRMWARE_TASK : creates + IOT_FIRMWARE_TASK ||--o{ IOT_FIRMWARE_TASK_DETAIL : tracks + IOT_DEVICE ||--o{ IOT_FIRMWARE_TASK_DETAIL : target + + IOT_FIRMWARE { + bigint firmware_id PK + bigint product_id + bigint tenant_id + } + IOT_FIRMWARE_TASK { + bigint id PK + bigint firmware_id + } + IOT_FIRMWARE_TASK_DETAIL { + bigint id PK + bigint task_id + varchar serial_number + varchar message_id + } +``` + +## SceneModel、SCADA、GoView + +```mermaid +erDiagram + SCENE_MODEL ||--o{ SCENE_MODEL_DEVICE : has + SCENE_MODEL ||--o{ SCENE_MODEL_DATA : has_data + SCENE_MODEL_DEVICE ||--o{ SCENE_MODEL_DATA : source + SCENE_MODEL ||--o{ SCENE_MODEL_TAG : has_tag + SCENE_MODEL_TAG ||--o{ SCENE_TAG_POINTS : calculated_from + SCENE_MODEL_DATA ||--o{ SCENE_TAG_POINTS : point_source + IOT_GOVIEW_PROJECT ||--o{ IOT_GOVIEW_PROJECT_DATA : has_data + SCADA ||--o{ SCADA_DEVICE_BIND : binds_by_guid + IOT_DEVICE ||--o{ SCADA_DEVICE_BIND : bound + SYS_USER ||--o{ SCADA_COMPONENT : owns + + SCENE_MODEL { + bigint scene_model_id PK + bigint tenant_id + } + SCENE_MODEL_DEVICE { + bigint id PK + bigint scene_model_id + bigint cus_device_id + } + SCENE_MODEL_DATA { + bigint id PK + bigint scene_model_id + bigint scene_model_device_id + bigint datasource_id + } + SCENE_MODEL_TAG { + bigint id PK + bigint scene_model_id + } + SCENE_TAG_POINTS { + bigint id PK + bigint tag_id + bigint scene_model_data_id + } + IOT_GOVIEW_PROJECT { + bigint id PK + bigint tenant_id + } + IOT_GOVIEW_PROJECT_DATA { + bigint id PK + bigint project_id + } + SCADA { + bigint id PK + bigint tenant_id + } + SCADA_COMPONENT { + bigint id PK + bigint tenant_id + bigint user_id + } + SCADA_DEVICE_BIND { + bigint id PK + varchar serial_number + varchar scada_guid + } + SCADA_ECHART { + bigint id PK + bigint tenant_id + } + SCADA_GALLERY { + bigint id PK + bigint tenant_id + } + SCADA_MODEL { + bigint id PK + bigint tenant_id + } +``` + +## 视频 GB28181 与媒体 + +```mermaid +erDiagram + IOT_PRODUCT ||--o{ SIP_CONFIG : config_for + IOT_PRODUCT ||--o{ SIP_DEVICE : product_device + SIP_DEVICE ||--o{ SIP_DEVICE_CHANNEL : has_channel + SIP_DEVICE_CHANNEL ||--o{ IOT_SIP_RELATION : bind_channel + IOT_DEVICE ||--o{ IOT_SIP_RELATION : bind_iot_device + SCENE_MODEL ||--o{ IOT_SIP_RELATION : bind_scene + MEDIA_SERVER ||--o{ SIP_DEVICE_CHANNEL : streams + + SIP_CONFIG { + bigint id PK + bigint product_id + } + SIP_DEVICE { + bigint device_id PK + bigint product_id + varchar device_sip_id + } + SIP_DEVICE_CHANNEL { + bigint id PK + varchar device_sip_id PK + varchar channel_sip_id + bigint product_id + bigint user_id + } + MEDIA_SERVER { + bigint id PK + varchar server_id + bigint tenant_id + } + IOT_SIP_RELATION { + bigint id PK + varchar channel_id + bigint re_device_id + bigint re_scene_model_id + } +``` + +## 系统 RBAC + +```mermaid +erDiagram + SYS_DEPT ||--o{ SYS_DEPT : parent + SYS_DEPT ||--o{ SYS_USER : contains + SYS_USER ||--o{ SYS_USER_ROLE : has + SYS_ROLE ||--o{ SYS_USER_ROLE : assigned + SYS_ROLE ||--o{ SYS_ROLE_MENU : grants + SYS_MENU ||--o{ SYS_ROLE_MENU : granted + SYS_MENU ||--o{ SYS_MENU : parent + SYS_ROLE ||--o{ SYS_ROLE_DEPT : data_scope + SYS_DEPT ||--o{ SYS_ROLE_DEPT : scoped + SYS_USER ||--o{ SYS_USER_POST : holds + SYS_POST ||--o{ SYS_USER_POST : assigned + SYS_USER ||--o{ SYS_USER_RELATION : hierarchy + SYS_DICT_TYPE ||--o{ SYS_DICT_DATA : has + SYS_DICT_TYPE ||--o{ SYS_DICT_TYPE_TRANSLATE : translated + SYS_DICT_DATA ||--o{ SYS_DICT_DATA_TRANSLATE : translated + SYS_MENU ||--o{ SYS_MENU_TRANSLATE : translated + + SYS_USER { + bigint user_id PK + bigint dept_id + } + SYS_DEPT { + bigint dept_id PK + bigint parent_id + } + SYS_ROLE { + bigint role_id PK + } + SYS_MENU { + bigint menu_id PK + bigint parent_id + } + SYS_POST { + bigint post_id PK + } + SYS_USER_ROLE { + bigint user_id PK + bigint role_id PK + } + SYS_ROLE_MENU { + bigint role_id PK + bigint menu_id PK + } + SYS_ROLE_DEPT { + bigint role_id PK + bigint dept_id PK + } + SYS_USER_POST { + bigint user_id PK + bigint post_id PK + } + SYS_DICT_TYPE { + bigint dict_id PK + varchar dict_type UK + } + SYS_DICT_DATA { + bigint dict_code PK + varchar dict_type + } +``` + +## OAuth、第三方登录、App 偏好 + +```mermaid +erDiagram + SYS_USER ||--o{ SYS_AUTH_USER : auth_binding + SYS_USER ||--o{ IOT_SOCIAL_USER : social_binding + IOT_SOCIAL_PLATFORM ||--o{ IOT_SOCIAL_USER : platform + OAUTH_CLIENT_DETAILS ||--o{ OAUTH_ACCESS_TOKEN : issues + OAUTH_CLIENT_DETAILS ||--o{ OAUTH_REFRESH_TOKEN : refreshes + OAUTH_CLIENT_DETAILS ||--o{ OAUTH_CLIENT_TOKEN : client_token + SYS_USER ||--o{ OAUTH_ACCESS_TOKEN : token_user + SYS_USER ||--o{ APP_PREFERENCES : preferences + APP_LANGUAGE ||--o{ APP_PREFERENCES : selected_language + + SYS_AUTH_USER { + bigint auth_id PK + bigint user_id + } + IOT_SOCIAL_PLATFORM { + bigint social_platform_id PK + varchar platform UK + } + IOT_SOCIAL_USER { + bigint social_user_id PK + bigint sys_user_id + varchar open_id + varchar union_id + } + OAUTH_CLIENT_DETAILS { + bigint id PK + varchar client_id + bigint tenant_id + } + OAUTH_ACCESS_TOKEN { + varchar token_id + varchar client_id + varchar user_id + } + APP_LANGUAGE { + bigint id PK + } + APP_PREFERENCES { + bigint id PK + bigint user_id + varchar language + } +``` + +## 内容、OSS、代码生成 + +```mermaid +erDiagram + NEWS_CATEGORY ||--o{ NEWS : contains + OSS_CONFIG ||--o{ OSS_DETAIL : stores + GEN_TABLE ||--o{ GEN_TABLE_COLUMN : has_column + SYS_DICT_TYPE ||--o{ GEN_TABLE_COLUMN : dict + + NEWS_CATEGORY { + bigint category_id PK + } + NEWS { + bigint news_id PK + bigint category_id + } + OSS_CONFIG { + bigint id PK + bigint tenant_id + } + OSS_DETAIL { + bigint id PK + bigint tenant_id + } + GEN_TABLE { + bigint table_id PK + } + GEN_TABLE_COLUMN { + bigint column_id PK + bigint table_id + varchar dict_type + } +``` + +## Quartz 真实外键图 + +```mermaid +erDiagram + QRTZ_JOB_DETAILS ||--o{ QRTZ_TRIGGERS : has + QRTZ_TRIGGERS ||--o| QRTZ_BLOB_TRIGGERS : blob + QRTZ_TRIGGERS ||--o| QRTZ_CRON_TRIGGERS : cron + QRTZ_TRIGGERS ||--o| QRTZ_SIMPLE_TRIGGERS : simple + QRTZ_TRIGGERS ||--o| QRTZ_SIMPROP_TRIGGERS : simprop + + QRTZ_JOB_DETAILS { + varchar sched_name PK + varchar job_name PK + varchar job_group PK + } + QRTZ_TRIGGERS { + varchar sched_name PK + varchar trigger_name PK + varchar trigger_group PK + varchar job_name FK + varchar job_group FK + } + QRTZ_BLOB_TRIGGERS { + varchar sched_name PK + varchar trigger_name PK + varchar trigger_group PK + } + QRTZ_CRON_TRIGGERS { + varchar sched_name PK + varchar trigger_name PK + varchar trigger_group PK + } + QRTZ_SIMPLE_TRIGGERS { + varchar sched_name PK + varchar trigger_name PK + varchar trigger_group PK + } + QRTZ_SIMPROP_TRIGGERS { + varchar sched_name PK + varchar trigger_name PK + varchar trigger_group PK + } +``` + +## 读图说明 + +- `||--o{` 表示一对多业务关系。 +- `||--o|` 表示一对零或一的扩展关系。 +- 图中 `PK`、`UK`、`FK` 是阅读标注;除 Quartz 图外,`FK` 不代表数据库中存在外键约束。 +- 对于 `serial_number` 关联,运行时经常直接使用设备编号查找数据,但数据建模上仍应优先理解 `iot_device.device_id` 是设备主键。 diff --git a/docs/knowledge-base/05-data-bridge-integration.md b/docs/knowledge-base/05-data-bridge-integration.md new file mode 100644 index 00000000..b9f6a1c2 --- /dev/null +++ b/docs/knowledge-base/05-data-bridge-integration.md @@ -0,0 +1,663 @@ +# 数据桥接对接文档 + +本文面向需要和 岩安测控 对接的第三方系统,覆盖两类能力: + +1. 第三方系统查询设备及其物模型。 +2. 第三方系统获取设备上报数据,包括 岩安测控 主动推送和第三方主动查询。 + +本文基于当前仓库已有接口和数据桥接实现整理。默认服务根地址记为 `{BASE_URL}`,例如 `http://1.116.124.85:8080`。 + +## 对接方式总览 + +```mermaid +flowchart LR + Device[设备] --> Protocol[MQTT/TCP/HTTP/CoAP 等接入] + Protocol --> 岩安测控[岩安测控 数据处理] + 岩安测控 --> Store[(MySQL/TDengine)] + 岩安测控 --> Rule[数据流脚本/规则引擎] + Rule --> PushHTTP[主动推送 HTTP] + Rule --> PushMQTT[主动桥接 MQTT] + Rule --> PushDB[写入第三方数据库] + Third[第三方系统] --> API[REST API 主动查询] + API --> 岩安测控 +``` + +| 场景 | 推荐方式 | 说明 | +| --- | --- | --- | +| 获取设备清单 | REST API 主动查询 | 查询 `iot_device` 设备列表或按设备编号查询详情。 | +| 获取设备物模型 | REST API 主动查询 | 查询产品级/设备级物模型,包含属性、事件、服务、计算模型。 | +| 获取实时值 | REST API 主动查询 | 查询设备运行时缓存值。 | +| 获取历史上报数据 | REST API 主动查询 | 查询数据中心历史接口,支持按设备、时间范围、物模型标识符过滤。 | +| 上报数据实时同步给第三方 | 数据桥接主动推送 | 通过数据流脚本触发 HTTP、MQTT 或数据库桥接。 | +| 第三方系统周期同步 | REST API 定时拉取 | 第三方按时间窗口分页/分段查询历史数据。 | + +## 认证 + +除安全配置中明确放行的匿名接口外,REST API 默认需要登录 Token。 + +### 登录获取 Token + +```http +POST {BASE_URL}/login +Content-Type: application/json + +{ + "username": "admin", + "password": "admin123", + "code": "验证码", + "uuid": "验证码uuid" +} +``` + +成功响应: + +```json +{ + "code": 200, + "msg": "操作成功", + "token": "eyJhbGciOiJIUzUxMiJ9..." +} +``` + +后续请求头: + +```http +Authorization: Bearer {token} +``` + +如果系统关闭验证码或使用内部服务账号,可按部署配置调整登录参数。第三方生产对接建议单独创建服务账号,并只授予以下最小权限: + +| 能力 | 权限标识 | +| --- | --- | +| 设备查询 | `iot:device:list`, `iot:device:query` | +| 物模型查询 | `iot:model:list`, `iot:model:query` | +| 历史数据查询 | `dataCenter:history:list` | +| 数据分析查询 | `dataCenter:analysis:list` | +| 数据桥接配置 | `iot:bridge:list`, `iot:bridge:add`, `iot:bridge:edit`, `iot:bridge:query` | +| 数据流脚本配置 | `iot:script:list`, `iot:script:add`, `iot:script:edit`, `iot:script:query` | + +## 通用响应 + +普通接口返回 `AjaxResult`: + +```json +{ + "code": 200, + "msg": "操作成功", + "data": {} +} +``` + +分页接口返回 `TableDataInfo`: + +```json +{ + "code": 200, + "msg": "查询成功", + "total": 100, + "rows": [] +} +``` + +常见约定: + +- `code=200` 表示成功。 +- 设备编号 `serialNumber` 在上报和查询中通常会转为大写处理,第三方建议统一传大写。 +- 时间字段建议使用 `yyyy-MM-dd HH:mm:ss`。 + +## 查询设备 + +### 设备分页列表 + +```http +GET {BASE_URL}/iot/device/list?pageNum=1&pageSize=20&deviceName=xxx&serialNumber=ABC001&productId=1 +Authorization: Bearer {token} +``` + +说明: + +- Controller:`DeviceController.list` +- 权限:`iot:device:list` +- 返回:分页设备列表。 +- 常用过滤字段来自 `Device` 对象,实际可用字段包括 `deviceId`、`deviceName`、`serialNumber`、`productId`、`tenantId`、`deptId`、`status` 等。 + +### 设备简短列表 + +适合第三方做设备下拉或轻量同步。 + +```http +GET {BASE_URL}/iot/device/shortList?pageNum=1&pageSize=100&productId=1 +Authorization: Bearer {token} +``` + +### 查询所有设备简短列表 + +```http +GET {BASE_URL}/iot/device/all +Authorization: Bearer {token} +``` + +### 按设备 ID 查询详情 + +```http +GET {BASE_URL}/iot/device/{deviceId} +Authorization: Bearer {token} +``` + +### 按设备编号查询详情 + +```http +GET {BASE_URL}/iot/device/getDeviceBySerialNumber/{serialNumber} +Authorization: Bearer {token} +``` + +示例: + +```http +GET {BASE_URL}/iot/device/getDeviceBySerialNumber/ABC001 +Authorization: Bearer {token} +``` + +## 查询设备物模型 + +岩安测控 的物模型主要由 `productId + identifier` 定义。设备级计算物模型或特殊覆盖会结合 `serialNumber` 处理。 + +### 按产品 ID 和设备编号查询物模型 + +推荐第三方使用该接口,它会从缓存中合并产品级物模型和设备级处理结果。 + +```http +GET {BASE_URL}/iot/model/listByProductIdAndSerialNumber?productId=1&serialNumber=ABC001 +Authorization: Bearer {token} +``` + +说明: + +- Controller:`ThingsModelController.listByProductIdAndSerialNumber` +- 权限:`iot:model:list` +- 返回:`ThingsModelDTO` 列表。 + +典型返回项字段: + +| 字段 | 含义 | +| --- | --- | +| `modelId` | 物模型 ID。 | +| `identifier` | 物模型标识符,历史数据查询和推送数据中使用该字段作为变量名。 | +| `modelName` | 物模型名称。 | +| `type` | 物模型类型,通常表示属性/功能/事件等。 | +| `datatype` | 数据类型定义,例如 number、string、bool、enum 等。 | +| `specs` | 枚举、单位、取值范围等扩展规格。 | +| `modelOrder` | 排序。 | + +### 查询设备变量概况 + +```http +GET {BASE_URL}/iot/device/listThingsModel?deviceId=100&pageNum=1&pageSize=50 +Authorization: Bearer {token} +``` + +可选过滤: + +- `modelName`:按物模型名称模糊过滤。 +- `type`:按物模型类型过滤。 + +### 查询产品物模型列表 + +```http +GET {BASE_URL}/iot/model/list?pageNum=1&pageSize=100&productId=1 +Authorization: Bearer {token} +``` + +### 获取缓存 JSON 物模型 + +```http +GET {BASE_URL}/iot/model/cache/{productId} +Authorization: Bearer {token} +``` + +适用于需要拿完整 TSL JSON 的场景。 + +## 查询实时数据 + +### 查询设备运行时状态 + +```http +GET {BASE_URL}/iot/runtime/running?serialNumber=ABC001 +Authorization: Bearer {token} +``` + +说明: + +- Controller:`DeviceRuntimeController.runState` +- 返回:物模型当前值列表。 +- 数据来源:运行时缓存/物模型缓存,不适合替代历史审计。 + +典型返回: + +```json +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "id": "temperature", + "name": "温度", + "value": "26.5", + "type": 1, + "datatype": { + "type": "decimal", + "unit": "℃" + } + } + ] +} +``` + +## 第三方主动查询设备上报数据 + +### 推荐接口:数据中心历史查询 + +```http +POST {BASE_URL}/data/center/deviceHistory +Authorization: Bearer {token} +Content-Type: application/json + +{ + "deviceId": 100, + "serialNumber": "ABC001", + "beginTime": "2026-05-24 00:00:00", + "endTime": "2026-05-24 23:59:59", + "identifierList": [ + { "identifier": "temperature", "type": 1 }, + { "identifier": "humidity", "type": 1 } + ] +} +``` + +说明: + +- Controller:`DataCenterController.deviceHistory` +- 权限:`dataCenter:history:list` +- `serialNumber` 必填。 +- `identifierList` 建议传入,避免一次拉取全部变量导致响应过大。 +- `type` 与物模型类型保持一致,常见属性数据为 `1`,以实际物模型返回值为准。 + +典型返回结构: + +```json +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "2026-05-24 10:00:00": [ + { "temperature": "26.5" }, + { "humidity": "60" } + ] + }, + { + "2026-05-24 10:01:00": [ + { "temperature": "26.7" }, + { "humidity": "59" } + ] + } + ] +} +``` + +### 设备日志列表 + +适合按原始日志粒度查询。 + +```http +GET {BASE_URL}/iot/deviceLog/list?pageNum=1&pageSize=50&serialNumber=ABC001&beginTime=2026-05-24 00:00:00&endTime=2026-05-24 23:59:59 +Authorization: Bearer {token} +``` + +说明: + +- Controller:`DeviceLogController.list` +- 权限:`iot:device:list` +- 返回:分页日志行,字段来自 `DeviceLog`。 + +### 设备历史数据 GET 查询 + +```http +GET {BASE_URL}/iot/deviceLog/history?serialNumber=ABC001&beginTime=2026-05-24 00:00:00&endTime=2026-05-24 23:59:59 +Authorization: Bearer {token} +``` + +返回按 `identifier` 分组的历史数据,适合较简单的查询场景。 + +### 按时间聚合的历史数据 + +```http +GET {BASE_URL}/iot/deviceLog/historyGroupByCreateTime?pageNum=1&pageSize=100&serialNumber=ABC001&beginTime=2026-05-24 00:00:00&endTime=2026-05-24 23:59:59 +Authorization: Bearer {token} +``` + +### 网关计算物模型历史 + +```http +POST {BASE_URL}/data/center/deviceHistoryCalculated +Authorization: Bearer {token} +Content-Type: application/json + +{ + "deviceId": 100, + "serialNumber": "GW001", + "beginTime": "2026-05-24 00:00:00", + "endTime": "2026-05-24 23:59:59", + "identifierList": [ + { "identifier": "avg_temperature", "type": 1 } + ] +} +``` + +### 周期拉取建议 + +第三方主动查询历史数据时,建议使用滑动时间窗口: + +1. 首次同步:按小时或天分段拉取。 +2. 增量同步:每 1 至 5 分钟拉取一次 `[lastSyncTime, now - 10s]`。 +3. 去重键:`serialNumber + identifier + time`。 +4. 重试:失败窗口不要丢弃,下次继续从 `lastSuccessEndTime` 拉取。 +5. 大设备量:先同步设备列表,再按设备分批拉取,避免单次请求过大。 + +## 岩安测控 主动推送给第三方系统 + +岩安测控 当前主动推送走“数据流脚本 + 数据桥接”机制。设备上报后,`RuleProcess.processRuleScript(...)` 会按产品和事件类型找到启用的数据流脚本,然后执行脚本动作: + +| 脚本动作 | `scriptAction` | 桥接组件 | 用途 | +| --- | ---: | --- | --- | +| HTTP 推送 | 3 | `httpBridge` | 调用第三方 HTTP 接口。 | +| MQTT 桥接 | 4 | `mqttBridge` | 发布到第三方 MQTT Broker。 | +| 数据库存储 | 5 | `databaseBridge` | 写入第三方数据库。 | + +设备上报事件类型: + +| 事件 | `scriptEvent` | 说明 | +| --- | ---: | --- | +| 设备上报 | 1 | 设备属性/数据上报后触发。 | +| 平台下发 | 2 | 平台服务/指令下发前后触发。 | +| 设备上线 | 3 | 设备上线触发。 | +| 设备离线 | 4 | 设备离线触发。 | +| HTTP 桥接入口数据 | 5 | 第三方调用 `/bridge/get|post|put` 后触发。 | +| MQTT 桥接入口数据 | 6 | MQTT 输入桥接触发,按脚本配置处理。 | + +### 主动 HTTP 推送配置 + +1. 新增数据桥接。 + +```http +POST {BASE_URL}/iot/bridge +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "推送到第三方业务系统", + "enable": "1", + "type": 3, + "direction": 2, + "route": "https://third.example.com/iot/report", + "configJson": "{\"method\":\"POST\",\"hostUrl\":\"https://third.example.com/iot/report\",\"requestHeaders\":\"{\\\"Authorization\\\":\\\"Bearer xxx\\\"}\",\"requestQuerys\":\"{}\",\"requestConfig\":\"\",\"requestBody\":\"\"}" +} +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `type=3` | HTTP 推送。 | +| `direction=2` | 输出方向。 | +| `route` | 展示/路由字段,实际 HTTP 地址以 `configJson.hostUrl` 为准。 | +| `configJson.method` | HTTP 方法,如 `POST`。 | +| `configJson.hostUrl` | 第三方接收地址。 | +| `configJson.requestHeaders` | 请求头 JSON 字符串。 | +| `configJson.requestQuerys` | Query 参数 JSON 字符串。 | +| `configJson.requestBody` | 自定义请求体模板;为空时默认使用脚本上下文 payload。 | + +2. 新增数据流脚本。 + +下面脚本展示的是核心意图:取得 `MsgContext`,设置桥接 ID,必要时重写 `payload/topic`。不同 LiteFlow 脚本语言的上下文变量写法可能不同,落地时以当前环境脚本校验结果为准。 + +```http +POST {BASE_URL}/iot/script +Authorization: Bearer {token} +Content-Type: application/json + +{ + "applicationName": "岩安测控", + "scriptName": "设备上报转第三方HTTP", + "scriptType": "script", + "scriptLanguage": "groovy", + "scriptPurpose": 1, + "scriptEvent": 1, + "scriptAction": 3, + "productId": 1, + "enable": 1, + "bridgeId": 10, + "scriptData": "def cxt = _meta.getContextBean(com.岩安测控.ruleEngine.context.MsgContext.class); cxt.setPayload(cxt.getPayload()); cxt.setData('httpBridgeID', 10);" +} +``` + +脚本关键点: + +- `scriptPurpose=1` 表示数据流。 +- `scriptEvent=1` 表示设备上报触发。 +- `scriptAction=3` 表示执行 HTTP 推送组件。 +- `productId` 表示该产品下设备上报时触发。 +- 脚本必须向上下文写入 `httpBridgeID`,值为桥接配置 `bridge.id`;HTTP 桥接组件会从上下文读取该 ID。 +- 脚本可改写 `payload`,从而控制推送给第三方的报文。 + +### 推荐 HTTP 推送报文 + +如果第三方希望使用稳定格式,建议脚本把 payload 改造成如下 JSON: + +```json +{ + "event": "property.post", + "productId": 1, + "serialNumber": "ABC001", + "protocolCode": "ModbusRtu", + "reportTime": "2026-05-24 10:00:00", + "data": [ + { + "identifier": "temperature", + "value": "26.5" + }, + { + "identifier": "humidity", + "value": "60" + } + ], + "rawPayload": "[{\"id\":\"temperature\",\"value\":\"26.5\"}]" +} +``` + +当前默认上下文 payload 通常是设备上报解析后的物模型数据 JSON,字段形态可能随协议解析结果变化。生产对接建议在脚本中统一出站格式,避免第三方直接依赖内部 payload。 + +### 主动 MQTT 桥接配置 + +1. 新增 MQTT 桥接。 + +```http +POST {BASE_URL}/iot/bridge +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "第三方MQTT桥接", + "enable": "1", + "type": 4, + "direction": 2, + "route": "third/iot/report", + "configJson": "{\"name\":\"third-mqtt\",\"hostUrl\":\"broker.example.com:1883\",\"username\":\"user\",\"password\":\"pass\",\"clientId\":\"岩安测控-bridge\"}" +} +``` + +2. 新增脚本。 + +```json +{ + "applicationName": "岩安测控", + "scriptName": "设备上报转第三方MQTT", + "scriptType": "script", + "scriptLanguage": "groovy", + "scriptPurpose": 1, + "scriptEvent": 1, + "scriptAction": 4, + "productId": 1, + "enable": 1, + "bridgeId": 11, + "scriptData": "def cxt = _meta.getContextBean(com.岩安测控.ruleEngine.context.MsgContext.class); cxt.setTopic('third/iot/' + cxt.getSerialNumber() + '/property/post'); cxt.setData('mqttBridgeID', 11);" +} +``` + +说明: + +- MQTT 桥接组件使用 `cxt.getTopic()` 作为发布主题。 +- 脚本必须写入 `mqttBridgeID`;MQTT 桥接组件会从上下文读取该 ID。 +- `payload` 默认透传,建议按第三方格式重写。 + +### 写入第三方数据库 + +1. 配置数据库桥接数据源。 + +数据库桥接组件读取 `MultipleDataSource`,字段包括: + +| 字段 | 说明 | +| --- | --- | +| `databaseSource` | 数据库类型,支持 MySQL、SQLServer、Oracle、PostgreSQL、H2。 | +| `host` | 地址,包含主机和端口。 | +| `dataBaseName` | 数据库名。 | +| `userName` | 用户名。 | +| `password` | 密码。 | +| `sql` | 要执行的 SQL,可使用上下文占位符替换。 | + +2. 新增数据流脚本,写入 `databaseBridgeID`。 + +```json +{ + "scriptPurpose": 1, + "scriptEvent": 1, + "scriptAction": 5, + "productId": 1, + "enable": 1, + "scriptData": "def cxt = _meta.getContextBean(com.岩安测控.ruleEngine.context.MsgContext.class); cxt.setData('databaseBridgeID', 20);" +} +``` + +注意:数据库桥接当前通过拼接后的 SQL 执行,生产环境必须控制 SQL 模板来源和字段转义,避免把不可信 payload 直接拼入 SQL。 + +## 第三方调用 岩安测控 桥接入口 + +岩安测控 也提供了桥接入口,第三方可以把数据送入 岩安测控 的数据流脚本: + +| 方法 | 地址 | 说明 | +| --- | --- | --- | +| GET | `{BASE_URL}/bridge/get` | Query/Header 进入上下文。 | +| POST | `{BASE_URL}/bridge/post` | Body 进入上下文 payload。 | +| PUT | `{BASE_URL}/bridge/put` | Body 进入上下文 payload。 | + +这些入口会以: + +```text +scriptPurpose=1 +scriptEvent=5 +route=/bridge/get|/bridge/post|/bridge/put +``` + +查询并执行数据流脚本。 + +安全注意:当前安全配置未默认放行 `/bridge/**`,因此仍需要 Token,除非部署侧自行放开。 + +## 推荐对接方案 + +### 方案 A:实时同步优先 + +适用:第三方需要尽快拿到设备上报数据。 + +1. 第三方提供 HTTP 接收地址或 MQTT Broker。 +2. 岩安测控 配置 `bridge`。 +3. 岩安测控 为对应产品配置数据流脚本。 +4. 第三方收到推送后按 `serialNumber + identifier + reportTime` 入库去重。 +5. 第三方定时调用历史查询接口补偿漏推窗口。 + +推荐组合: + +- 实时:HTTP 推送或 MQTT 桥接。 +- 补偿:`POST /data/center/deviceHistory`。 + +### 方案 B:第三方定时拉取 + +适用:第三方不开放公网回调地址,或只需要分钟级/小时级同步。 + +1. 调用 `/iot/device/list` 或 `/iot/device/shortList` 同步设备。 +2. 调用 `/iot/model/listByProductIdAndSerialNumber` 同步物模型。 +3. 按设备调用 `/data/center/deviceHistory` 拉取上报数据。 +4. 使用滑动窗口和去重键保证幂等。 + +### 方案 C:第三方数据库落库 + +适用:第三方只提供数据库账号,不提供 API。 + +1. 配置数据库连接。 +2. 为产品配置数据流脚本。 +3. 数据库桥接执行 SQL 写入第三方库。 + +不建议把该方案作为首选,原因是数据库账号权限、SQL 模板安全和网络连通性风险更高。 + +## 字段映射建议 + +第三方侧建议建立如下最小模型。 + +### 设备表 + +| 字段 | 来源 | +| --- | --- | +| `device_id` | `iot_device.deviceId` | +| `serial_number` | `iot_device.serialNumber` | +| `device_name` | `iot_device.deviceName` | +| `product_id` | `iot_device.productId` | +| `status` | `iot_device.status` | +| `tenant_id` | `iot_device.tenantId` | + +### 物模型表 + +| 字段 | 来源 | +| --- | --- | +| `model_id` | `ThingsModelDTO.modelId` | +| `product_id` | 查询入参或返回字段 | +| `identifier` | `ThingsModelDTO.identifier` | +| `name` | `ThingsModelDTO.modelName` | +| `type` | `ThingsModelDTO.type` | +| `datatype` | `ThingsModelDTO.datatype` | + +### 设备数据表 + +| 字段 | 来源 | +| --- | --- | +| `serial_number` | 设备编号 | +| `product_id` | 设备所属产品 | +| `identifier` | 物模型标识符 | +| `value` | 上报值 | +| `report_time` | 历史接口返回的时间或推送报文时间 | +| `source` | `pull` 或 `push` | +| `raw_payload` | 原始推送/查询报文 | + +## 幂等、重试和安全 + +- 推送接口必须幂等,建议第三方以 `serialNumber + identifier + reportTime` 去重。 +- 岩安测控 主动推送失败当前主要依赖组件日志排查,建议第三方同时做主动拉取补偿。 +- 第三方主动查询必须按时间窗口限制范围,避免一次拉取全量历史。 +- HTTP 推送建议使用 HTTPS、固定 Token 或签名头。 +- MQTT 桥接建议使用独立账号和最小 Topic 权限。 +- 数据库桥接建议只授予 INSERT 权限,避免执行 DDL 或高危 SQL。 + +## 当前实现注意点 + +- `岩安测控-mq` 当前核心是 JVM 内本地阻塞队列,不是外部 RocketMQ 的强持久化链路。 +- 主动 HTTP 推送的默认 payload 由规则脚本上下文决定,生产对接应在脚本中统一出站报文。 +- 数据桥接脚本查询按 `productId + scriptPurpose + scriptEvent + enable` 等条件筛选;产品维度必须配置正确。 +- 业务表关系大多没有数据库外键,第三方不要依赖数据库外键判断关联,应以 API 返回和业务字段为准。 diff --git a/docs/knowledge-base/README.md b/docs/knowledge-base/README.md new file mode 100644 index 00000000..5d145ec5 --- /dev/null +++ b/docs/knowledge-base/README.md @@ -0,0 +1,25 @@ +# FastBee 项目知识库 + +本知识库用于快速理解 FastBee 当前仓库的整体架构、运行链路、模块职责和数据库关系。内容基于本仓库代码、Maven 配置、`application*.yml` 和 `sql/yack-20260316.sql` 梳理。 + +## 阅读路径 + +1. [整体架构](./01-architecture.md):项目分层、模块边界、依赖入口、前后端组成。 +2. [核心运行链路](./02-runtime-flows.md):启动、设备接入、上报、下发、规则引擎、视频和通知链路。 +3. [数据库与表关系](./03-data-model.md):表分组、核心实体、业务关联、真实外键和约定关联。 +4. [数据库关系图](./04-er-diagrams.md):按业务域拆分的 Mermaid ER 图。 +5. [数据桥接对接文档](./05-data-bridge-integration.md):设备/物模型查询、设备上报数据推送和第三方主动查询。 + +## 当前结论 + +- 后端是 Java 8 + Spring Boot 2.5.14 的 Maven 多模块工程,主入口是 `fastbee-admin`。 +- 前端包含三个独立应用:`yack-web` 管理后台、`yack-view` 低代码可视化、`yack-app` UniApp 移动端。 +- IoT 接入由 `fastbee-server` 下的 Netty 协议服务承载,协议编解码在 `fastbee-protocol`,业务处理集中在 `fastbee-iot-data` 和 `fastbee-service/fastbee-iot-service`。 +- 设备消息队列当前主要是 JVM 内本地 `LinkedBlockingQueue` 队列族,`fastbee-mq` 模块名称虽为 MQ,但核心代码不是外部 RocketMQ Consumer 模式。 +- 主库初始化脚本为 `sql/yack-20260316.sql`。脚本中大部分业务表没有数据库外键,关系依靠字段命名、联合主键、索引和业务代码约定;Quartz 表保留了真实外键约束。 + +## 维护约定 + +- 业务逻辑、表关系和配置变化后,同步更新本目录对应文档。 +- 表关系文档中用“强约束”表示 SQL 外键,用“业务约定”表示代码和字段约定形成的关系。 +- 不把推断关系写成数据库强约束,除非 SQL 中确实存在 `FOREIGN KEY`。 diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/integration/IntegrationPlatformController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/integration/IntegrationPlatformController.java new file mode 100644 index 00000000..fb47609f --- /dev/null +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/integration/IntegrationPlatformController.java @@ -0,0 +1,196 @@ +package com.fastbee.controller.integration; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fastbee.common.annotation.Anonymous; +import com.fastbee.common.core.controller.BaseController; +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.core.page.TableDataInfo; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.integration.IntegrationApi; +import com.fastbee.iot.domain.integration.IntegrationDeviceMapping; +import com.fastbee.iot.domain.integration.IntegrationLog; +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.model.integration.IntegrationSyncRequest; +import com.fastbee.iot.service.integration.IIntegrationPlatformService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 第三方平台接入。 + */ +@Api(tags = "第三方平台接入") +@RestController +@RequestMapping("/iot/integration") +public class IntegrationPlatformController extends BaseController { + + @Resource + private IIntegrationPlatformService integrationPlatformService; + + @ApiOperation("查询平台接入配置") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/platform/list") + public TableDataInfo list(IntegrationPlatform query) { + startPage(); + List list = integrationPlatformService.selectPlatformList(query); + return getDataTable(list); + } + + @ApiOperation("查询平台接入配置详情") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/platform/{id}") + public AjaxResult getPlatform(@PathVariable Long id) { + return AjaxResult.success(integrationPlatformService.selectPlatformById(id)); + } + + @ApiOperation("新增平台接入配置") + @PreAuthorize("@ss.hasPermi('iot:integration:add')") + @PostMapping("/platform") + public AjaxResult addPlatform(@RequestBody IntegrationPlatform platform) { + return toAjax(integrationPlatformService.insertPlatform(platform)); + } + + @ApiOperation("修改平台接入配置") + @PreAuthorize("@ss.hasPermi('iot:integration:edit')") + @PutMapping("/platform") + public AjaxResult editPlatform(@RequestBody IntegrationPlatform platform) { + return toAjax(integrationPlatformService.updatePlatform(platform)); + } + + @ApiOperation("删除平台接入配置") + @PreAuthorize("@ss.hasPermi('iot:integration:remove')") + @DeleteMapping("/platform/{ids}") + public AjaxResult removePlatform(@PathVariable Long[] ids) { + return toAjax(integrationPlatformService.deletePlatformByIds(ids)); + } + + @ApiOperation("查询平台接口定义") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/api/list") + public TableDataInfo apiList(IntegrationApi query) { + startPage(); + List list = integrationPlatformService.selectApiList(query); + return getDataTable(list); + } + + @ApiOperation("查询平台接口定义详情") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/api/{id}") + public AjaxResult getApi(@PathVariable Long id) { + return AjaxResult.success(integrationPlatformService.selectApiById(id)); + } + + @ApiOperation("新增平台接口定义") + @PreAuthorize("@ss.hasPermi('iot:integration:add')") + @PostMapping("/api") + public AjaxResult addApi(@RequestBody IntegrationApi api) { + return toAjax(integrationPlatformService.insertApi(api)); + } + + @ApiOperation("修改平台接口定义") + @PreAuthorize("@ss.hasPermi('iot:integration:edit')") + @PutMapping("/api") + public AjaxResult editApi(@RequestBody IntegrationApi api) { + return toAjax(integrationPlatformService.updateApi(api)); + } + + @ApiOperation("删除平台接口定义") + @PreAuthorize("@ss.hasPermi('iot:integration:remove')") + @DeleteMapping("/api/{ids}") + public AjaxResult removeApi(@PathVariable Long[] ids) { + return toAjax(integrationPlatformService.deleteApiByIds(ids)); + } + + @ApiOperation("查询平台设备映射") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/device-mapping/list") + public TableDataInfo deviceMappingList(IntegrationDeviceMapping query) { + startPage(); + List list = integrationPlatformService.selectDeviceMappingList(query); + return getDataTable(list); + } + + @ApiOperation("查询平台设备映射详情") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/device-mapping/{id}") + public AjaxResult getDeviceMapping(@PathVariable Long id) { + return AjaxResult.success(integrationPlatformService.selectDeviceMappingById(id)); + } + + @ApiOperation("新增平台设备映射") + @PreAuthorize("@ss.hasPermi('iot:integration:add')") + @PostMapping("/device-mapping") + public AjaxResult addDeviceMapping(@RequestBody IntegrationDeviceMapping mapping) { + return toAjax(integrationPlatformService.insertDeviceMapping(mapping)); + } + + @ApiOperation("修改平台设备映射") + @PreAuthorize("@ss.hasPermi('iot:integration:edit')") + @PutMapping("/device-mapping") + public AjaxResult editDeviceMapping(@RequestBody IntegrationDeviceMapping mapping) { + return toAjax(integrationPlatformService.updateDeviceMapping(mapping)); + } + + @ApiOperation("删除平台设备映射") + @PreAuthorize("@ss.hasPermi('iot:integration:remove')") + @DeleteMapping("/device-mapping/{ids}") + public AjaxResult removeDeviceMapping(@PathVariable Long[] ids) { + return toAjax(integrationPlatformService.deleteDeviceMappingByIds(ids)); + } + + @ApiOperation("查询平台同步日志") + @PreAuthorize("@ss.hasPermi('iot:integration:list')") + @GetMapping("/log/list") + public TableDataInfo logList(IntegrationLog query) { + startPage(); + List list = integrationPlatformService.selectLogList(query); + return getDataTable(list); + } + + @ApiOperation("触发平台数据同步") + @PreAuthorize("@ss.hasPermi('iot:integration:sync')") + @PostMapping("/sync") + public AjaxResult sync(@RequestBody IntegrationSyncRequest request) { + return AjaxResult.success(integrationPlatformService.sync(request)); + } + + @Anonymous + @ApiOperation("第三方触发平台数据同步") + @PostMapping("/open/sync") + public AjaxResult openSync(@RequestHeader(value = "X-Integration-Key", required = false) String integrationKey, + @RequestBody IntegrationSyncRequest request) { + checkOpenKey(request, integrationKey); + return AjaxResult.success(integrationPlatformService.sync(request)); + } + + @ApiOperation("平台适配器自测") + @PreAuthorize("@ss.hasPermi('iot:integration:sync')") + @PostMapping("/self-test/{platformCode}") + public AjaxResult selfTest(@PathVariable String platformCode) { + return AjaxResult.success(integrationPlatformService.selfTest(platformCode)); + } + + private void checkOpenKey(IntegrationSyncRequest request, String integrationKey) { + if (request == null || StringUtils.isBlank(request.getPlatformCode())) { + throw new ServiceException("平台编码不能为空"); + } + IntegrationPlatform platform = integrationPlatformService.selectPlatformByCode(request.getPlatformCode()); + if (platform == null) { + throw new ServiceException("平台不存在"); + } + JSONObject credential = StringUtils.isBlank(platform.getCredentialJson()) ? new JSONObject() : JSON.parseObject(platform.getCredentialJson()); + String openApiKey = credential.getString("openApiKey"); + if (StringUtils.isBlank(openApiKey)) { + throw new ServiceException("平台未启用开放调度密钥"); + } + if (!openApiKey.equals(integrationKey)) { + throw new ServiceException("开放调度密钥无效"); + } + } +} diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/integration/dcp/DcpOpenApiController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/integration/dcp/DcpOpenApiController.java new file mode 100644 index 00000000..2c15c9ec --- /dev/null +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/integration/dcp/DcpOpenApiController.java @@ -0,0 +1,65 @@ +package com.fastbee.controller.integration.dcp; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.fastbee.common.annotation.Anonymous; +import com.fastbee.iot.service.integration.IDcpOpenApiService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * DCP轴力计兼容开放接口。 + * + *

接口路径与《DCP接口文档(轴力计)》保持一致,前缀为 /open/dcp。

+ */ +@Anonymous +@Api(tags = "DCP轴力计兼容开放接口") +@RestController +@RequestMapping("/open/dcp") +public class DcpOpenApiController { + + @Resource + private IDcpOpenApiService dcpOpenApiService; + + @ApiOperation("获取访问凭证") + @PostMapping("/connect/token") + public JSONObject token(@RequestParam Map form) { + return dcpOpenApiService.createToken(form == null ? new HashMap<>() : form); + } + + @ApiOperation("获取用户信息") + @GetMapping("/api/app/user/user-id") + public JSONObject userId(@RequestHeader("Authorization") String authorization) { + return dcpOpenApiService.getUser(authorization); + } + + @ApiOperation("获取项目信息") + @GetMapping("/api/app/project/by-user-id/{userId}") + public JSONArray projects(@RequestHeader("Authorization") String authorization, + @PathVariable String userId) { + return dcpOpenApiService.getProjects(authorization, userId); + } + + @ApiOperation("获取项目轴力计测点") + @PostMapping("/api/app/stress-sensor/by-project-id/{projectId}") + public JSONArray stressSensors(@RequestHeader("Authorization") String authorization, + @PathVariable String projectId) { + return dcpOpenApiService.getStressSensors(authorization, projectId); + } + + @ApiOperation("批量获取轴力计数据") + @PostMapping("/api/app/inclino-data/load-dataset-by-device-ids") + public JSONObject dataset(@RequestHeader("Authorization") String authorization, + @RequestParam String startTime, + @RequestParam String endTime, + @RequestParam(defaultValue = "true") Boolean isOriginal, + @RequestBody List deviceIds) { + return dcpOpenApiService.getDataset(authorization, startTime, endTime, isOriginal, deviceIds); + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationApi.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationApi.java new file mode 100644 index 00000000..8ce83030 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationApi.java @@ -0,0 +1,68 @@ +package com.fastbee.iot.domain.integration; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fastbee.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 第三方平台接口定义。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("iot_integration_api") +@ApiModel(value = "IntegrationApi", description = "第三方平台接口定义") +public class IntegrationApi extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("主键") + private Long id; + + @ApiModelProperty("平台ID") + private Long platformId; + + @ApiModelProperty("接口编码") + private String apiCode; + + @ApiModelProperty("接口名称") + private String apiName; + + @ApiModelProperty("请求方法") + private String method; + + @ApiModelProperty("接口 URI") + private String uri; + + @ApiModelProperty("Content-Type") + private String contentType; + + @ApiModelProperty("请求头模板 JSON") + private String headersJson; + + @ApiModelProperty("Query 模板 JSON") + private String queryJson; + + @ApiModelProperty("Body 模板 JSON") + private String bodyJson; + + @ApiModelProperty("返回数据路径") + private String responsePath; + + @ApiModelProperty("依赖接口编码") + private String dependsOn; + + @ApiModelProperty("排序") + private Integer sortOrder; + + @ApiModelProperty("状态:0=停用 1=启用") + private Integer status; + + @ApiModelProperty("删除标志:0=存在 2=删除") + private String delFlag; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationDeviceMapping.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationDeviceMapping.java new file mode 100644 index 00000000..00370451 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationDeviceMapping.java @@ -0,0 +1,50 @@ +package com.fastbee.iot.domain.integration; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fastbee.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 第三方设备与本系统设备映射。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("iot_integration_device_mapping") +@ApiModel(value = "IntegrationDeviceMapping", description = "第三方设备映射") +public class IntegrationDeviceMapping extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("主键") + private Long id; + + @ApiModelProperty("平台ID") + private Long platformId; + + @ApiModelProperty("第三方设备ID") + private String externalDeviceId; + + @ApiModelProperty("第三方设备名称") + private String externalDeviceName; + + @ApiModelProperty("本系统产品ID") + private Long productId; + + @ApiModelProperty("本系统设备编号") + private String serialNumber; + + @ApiModelProperty("指标映射 JSON,sourceField -> identifier") + private String identifierMappingJson; + + @ApiModelProperty("状态:0=停用 1=启用") + private Integer status; + + @ApiModelProperty("删除标志:0=存在 2=删除") + private String delFlag; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationLog.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationLog.java new file mode 100644 index 00000000..04b9a66e --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationLog.java @@ -0,0 +1,61 @@ +package com.fastbee.iot.domain.integration; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fastbee.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 第三方平台接入执行日志。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("iot_integration_log") +@ApiModel(value = "IntegrationLog", description = "第三方平台接入执行日志") +public class IntegrationLog extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("主键") + private Long id; + + @ApiModelProperty("平台ID") + private Long platformId; + + @ApiModelProperty("平台编码") + private String platformCode; + + @ApiModelProperty("流程编码") + private String flowCode; + + @ApiModelProperty("请求ID") + private String requestId; + + @ApiModelProperty("开始时间") + private Date startTime; + + @ApiModelProperty("结束时间") + private Date endTime; + + @ApiModelProperty("状态:0=失败 1=成功 2=执行中") + private Integer status; + + @ApiModelProperty("成功数量") + private Integer successCount; + + @ApiModelProperty("失败数量") + private Integer failCount; + + @ApiModelProperty("错误信息") + private String errorMsg; + + @ApiModelProperty("详情 JSON") + private String detailJson; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationPlatform.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationPlatform.java new file mode 100644 index 00000000..bbf7b339 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/integration/IntegrationPlatform.java @@ -0,0 +1,47 @@ +package com.fastbee.iot.domain.integration; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fastbee.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 第三方平台接入配置。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("iot_integration_platform") +@ApiModel(value = "IntegrationPlatform", description = "第三方平台接入配置") +public class IntegrationPlatform extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("主键") + private Long id; + + @ApiModelProperty("平台编码") + private String platformCode; + + @ApiModelProperty("平台名称") + private String platformName; + + @ApiModelProperty("基础地址") + private String baseUrl; + + @ApiModelProperty("认证类型") + private String authType; + + @ApiModelProperty("凭证配置 JSON") + private String credentialJson; + + @ApiModelProperty("状态:0=停用 1=启用") + private Integer status; + + @ApiModelProperty("删除标志:0=存在 2=删除") + private String delFlag; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationApiMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationApiMapper.java new file mode 100644 index 00000000..883144e6 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationApiMapper.java @@ -0,0 +1,9 @@ +package com.fastbee.iot.mapper.integration; + +import com.fastbee.framework.mybatis.mapper.BaseMapperX; +import com.fastbee.iot.domain.integration.IntegrationApi; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegrationApiMapper extends BaseMapperX { +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationDeviceMappingMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationDeviceMappingMapper.java new file mode 100644 index 00000000..8161b1cc --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationDeviceMappingMapper.java @@ -0,0 +1,9 @@ +package com.fastbee.iot.mapper.integration; + +import com.fastbee.framework.mybatis.mapper.BaseMapperX; +import com.fastbee.iot.domain.integration.IntegrationDeviceMapping; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegrationDeviceMappingMapper extends BaseMapperX { +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationLogMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationLogMapper.java new file mode 100644 index 00000000..e00e41a5 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationLogMapper.java @@ -0,0 +1,9 @@ +package com.fastbee.iot.mapper.integration; + +import com.fastbee.framework.mybatis.mapper.BaseMapperX; +import com.fastbee.iot.domain.integration.IntegrationLog; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegrationLogMapper extends BaseMapperX { +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationPlatformMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationPlatformMapper.java new file mode 100644 index 00000000..85519610 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/integration/IntegrationPlatformMapper.java @@ -0,0 +1,9 @@ +package com.fastbee.iot.mapper.integration; + +import com.fastbee.framework.mybatis.mapper.BaseMapperX; +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegrationPlatformMapper extends BaseMapperX { +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncRequest.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncRequest.java new file mode 100644 index 00000000..f462c823 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncRequest.java @@ -0,0 +1,40 @@ +package com.fastbee.iot.model.integration; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 第三方平台同步请求。 + */ +@Data +@ApiModel(value = "IntegrationSyncRequest", description = "第三方平台同步请求") +public class IntegrationSyncRequest { + + @ApiModelProperty("平台编码") + private String platformCode; + + @ApiModelProperty("开始时间,ISO-8601 格式") + private String startTime; + + @ApiModelProperty("结束时间,ISO-8601 格式") + private String endTime; + + @ApiModelProperty("是否原始数据") + private Boolean original; + + @ApiModelProperty("是否只演练不写入") + private Boolean dryRun; + + @ApiModelProperty("第三方项目ID,未传则使用平台流程获取") + private String projectId; + + @ApiModelProperty("指定第三方设备ID") + private List externalDeviceIds; + + @ApiModelProperty("运行时参数") + private Map params; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncResult.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncResult.java new file mode 100644 index 00000000..776a2d37 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/IntegrationSyncResult.java @@ -0,0 +1,57 @@ +package com.fastbee.iot.model.integration; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 第三方平台同步结果。 + */ +@Data +@ApiModel(value = "IntegrationSyncResult", description = "第三方平台同步结果") +public class IntegrationSyncResult { + + @ApiModelProperty("平台编码") + private String platformCode; + + @ApiModelProperty("流程编码") + private String flowCode; + + @ApiModelProperty("请求ID") + private String requestId; + + @ApiModelProperty("是否演练") + private boolean dryRun; + + @ApiModelProperty("成功数量") + private int successCount; + + @ApiModelProperty("失败数量") + private int failCount; + + @ApiModelProperty("设备数量") + private int deviceCount; + + @ApiModelProperty("数据数量") + private int dataCount; + + @ApiModelProperty("消息") + private String message; + + @ApiModelProperty("详情") + private List details = new ArrayList<>(); + + public void addSuccess() { + successCount++; + } + + public void addFail(String detail) { + failCount++; + if (detail != null) { + details.add(detail); + } + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/UnifiedTelemetry.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/UnifiedTelemetry.java new file mode 100644 index 00000000..7699cc14 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/integration/UnifiedTelemetry.java @@ -0,0 +1,26 @@ +package com.fastbee.iot.model.integration; + +import lombok.Data; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 平台接入后的统一遥测数据。 + */ +@Data +public class UnifiedTelemetry { + + private String platformCode; + + private String externalDeviceId; + + private String externalDeviceName; + + private Date reportTime; + + private Map values = new LinkedHashMap<>(); + + private Object raw; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/IntegrationPlatformServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/IntegrationPlatformServiceImpl.java new file mode 100644 index 00000000..b2978152 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/IntegrationPlatformServiceImpl.java @@ -0,0 +1,434 @@ +package com.fastbee.iot.service.impl.integration; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.integration.IntegrationApi; +import com.fastbee.iot.domain.integration.IntegrationDeviceMapping; +import com.fastbee.iot.domain.integration.IntegrationLog; +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.mapper.integration.IntegrationApiMapper; +import com.fastbee.iot.mapper.integration.IntegrationDeviceMappingMapper; +import com.fastbee.iot.mapper.integration.IntegrationLogMapper; +import com.fastbee.iot.mapper.integration.IntegrationPlatformMapper; +import com.fastbee.iot.model.integration.IntegrationSyncRequest; +import com.fastbee.iot.model.integration.IntegrationSyncResult; +import com.fastbee.iot.service.integration.IIntegrationPlatformService; +import com.fastbee.iot.service.integration.PlatformAdapter; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 第三方平台接入服务。 + */ +@Service +public class IntegrationPlatformServiceImpl implements IIntegrationPlatformService { + + @Resource + private IntegrationPlatformMapper platformMapper; + + @Resource + private IntegrationApiMapper apiMapper; + + @Resource + private IntegrationDeviceMappingMapper deviceMappingMapper; + + @Resource + private IntegrationLogMapper logMapper; + + private final Map adapterMap = new HashMap<>(); + + public IntegrationPlatformServiceImpl(List adapters) { + if (adapters != null) { + for (PlatformAdapter adapter : adapters) { + adapterMap.put(adapter.platformCode().toUpperCase(), adapter); + } + } + } + + @Override + public IntegrationPlatform selectPlatformByCode(String platformCode) { + if (StringUtils.isBlank(platformCode)) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationPlatform::getPlatformCode, platformCode.toUpperCase()) + .eq(IntegrationPlatform::getDelFlag, "0") + .last("limit 1"); + return platformMapper.selectOne(wrapper); + } + + @Override + public IntegrationPlatform selectPlatformById(Long id) { + if (id == null) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationPlatform::getId, id) + .eq(IntegrationPlatform::getDelFlag, "0") + .last("limit 1"); + return platformMapper.selectOne(wrapper); + } + + @Override + public List selectPlatformList(IntegrationPlatform query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationPlatform::getDelFlag, "0"); + if (query != null) { + wrapper.like(StringUtils.isNotBlank(query.getPlatformName()), IntegrationPlatform::getPlatformName, query.getPlatformName()); + wrapper.eq(StringUtils.isNotBlank(query.getPlatformCode()), IntegrationPlatform::getPlatformCode, query.getPlatformCode()); + wrapper.eq(query.getStatus() != null, IntegrationPlatform::getStatus, query.getStatus()); + } + wrapper.orderByDesc(IntegrationPlatform::getCreateTime); + return platformMapper.selectList(wrapper); + } + + @Override + public int insertPlatform(IntegrationPlatform platform) { + checkPlatform(platform); + String platformCode = platform.getPlatformCode().toUpperCase(); + if (selectPlatformByCode(platformCode) != null) { + throw new ServiceException("平台编码已存在:" + platformCode); + } + platform.setPlatformCode(platformCode); + platform.setCredentialJson(normalizeJson(platform.getCredentialJson(), "凭证配置")); + if (platform.getStatus() == null) { + platform.setStatus(0); + } + platform.setDelFlag("0"); + platform.setCreateTime(new Date()); + return platformMapper.insert(platform); + } + + @Override + public int updatePlatform(IntegrationPlatform platform) { + if (platform == null || platform.getId() == null) { + throw new ServiceException("平台ID不能为空"); + } + checkPlatform(platform); + String platformCode = platform.getPlatformCode().toUpperCase(); + IntegrationPlatform exists = selectPlatformByCode(platformCode); + if (exists != null && !exists.getId().equals(platform.getId())) { + throw new ServiceException("平台编码已存在:" + platformCode); + } + platform.setPlatformCode(platformCode); + platform.setCredentialJson(normalizeJson(platform.getCredentialJson(), "凭证配置")); + platform.setUpdateTime(new Date()); + return platformMapper.updateById(platform); + } + + @Override + public int deletePlatformByIds(Long[] ids) { + int rows = 0; + if (ids == null) { + return rows; + } + for (Long id : ids) { + if (id == null) { + continue; + } + IntegrationPlatform platform = new IntegrationPlatform(); + platform.setId(id); + platform.setDelFlag("2"); + platform.setUpdateTime(new Date()); + rows += platformMapper.updateById(platform); + } + return rows; + } + + @Override + public List selectApiList(IntegrationApi query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationApi::getDelFlag, "0"); + if (query != null) { + wrapper.eq(query.getPlatformId() != null, IntegrationApi::getPlatformId, query.getPlatformId()); + wrapper.like(StringUtils.isNotBlank(query.getApiCode()), IntegrationApi::getApiCode, query.getApiCode()); + wrapper.like(StringUtils.isNotBlank(query.getApiName()), IntegrationApi::getApiName, query.getApiName()); + wrapper.eq(query.getStatus() != null, IntegrationApi::getStatus, query.getStatus()); + } + wrapper.orderByAsc(IntegrationApi::getSortOrder).orderByAsc(IntegrationApi::getId); + return apiMapper.selectList(wrapper); + } + + @Override + public IntegrationApi selectApiById(Long id) { + if (id == null) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationApi::getId, id) + .eq(IntegrationApi::getDelFlag, "0") + .last("limit 1"); + return apiMapper.selectOne(wrapper); + } + + @Override + public int insertApi(IntegrationApi api) { + checkApi(api); + if (api.getStatus() == null) { + api.setStatus(1); + } + if (api.getSortOrder() == null) { + api.setSortOrder(0); + } + api.setApiCode(api.getApiCode().toUpperCase()); + api.setMethod(api.getMethod().toUpperCase()); + api.setHeadersJson(normalizeJson(api.getHeadersJson(), "请求头模板")); + api.setQueryJson(normalizeJson(api.getQueryJson(), "Query模板")); + api.setBodyJson(normalizeJson(api.getBodyJson(), "Body模板")); + api.setDelFlag("0"); + api.setCreateTime(new Date()); + return apiMapper.insert(api); + } + + @Override + public int updateApi(IntegrationApi api) { + if (api == null || api.getId() == null) { + throw new ServiceException("接口ID不能为空"); + } + checkApi(api); + api.setApiCode(api.getApiCode().toUpperCase()); + api.setMethod(api.getMethod().toUpperCase()); + api.setHeadersJson(normalizeJson(api.getHeadersJson(), "请求头模板")); + api.setQueryJson(normalizeJson(api.getQueryJson(), "Query模板")); + api.setBodyJson(normalizeJson(api.getBodyJson(), "Body模板")); + api.setUpdateTime(new Date()); + return apiMapper.updateById(api); + } + + @Override + public int deleteApiByIds(Long[] ids) { + int rows = 0; + if (ids == null) { + return rows; + } + for (Long id : ids) { + if (id == null) { + continue; + } + IntegrationApi api = new IntegrationApi(); + api.setId(id); + api.setDelFlag("2"); + api.setUpdateTime(new Date()); + rows += apiMapper.updateById(api); + } + return rows; + } + + @Override + public List selectDeviceMappingList(IntegrationDeviceMapping query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationDeviceMapping::getDelFlag, "0"); + if (query != null) { + wrapper.eq(query.getPlatformId() != null, IntegrationDeviceMapping::getPlatformId, query.getPlatformId()); + wrapper.like(StringUtils.isNotBlank(query.getExternalDeviceId()), IntegrationDeviceMapping::getExternalDeviceId, query.getExternalDeviceId()); + wrapper.like(StringUtils.isNotBlank(query.getExternalDeviceName()), IntegrationDeviceMapping::getExternalDeviceName, query.getExternalDeviceName()); + wrapper.eq(query.getProductId() != null, IntegrationDeviceMapping::getProductId, query.getProductId()); + wrapper.like(StringUtils.isNotBlank(query.getSerialNumber()), IntegrationDeviceMapping::getSerialNumber, query.getSerialNumber()); + wrapper.eq(query.getStatus() != null, IntegrationDeviceMapping::getStatus, query.getStatus()); + } + wrapper.orderByDesc(IntegrationDeviceMapping::getCreateTime); + return deviceMappingMapper.selectList(wrapper); + } + + @Override + public IntegrationDeviceMapping selectDeviceMappingById(Long id) { + if (id == null) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationDeviceMapping::getId, id) + .eq(IntegrationDeviceMapping::getDelFlag, "0") + .last("limit 1"); + return deviceMappingMapper.selectOne(wrapper); + } + + @Override + public int insertDeviceMapping(IntegrationDeviceMapping mapping) { + checkDeviceMapping(mapping); + if (mapping.getStatus() == null) { + mapping.setStatus(1); + } + mapping.setIdentifierMappingJson(normalizeJson(mapping.getIdentifierMappingJson(), "指标映射")); + mapping.setDelFlag("0"); + mapping.setCreateTime(new Date()); + return deviceMappingMapper.insert(mapping); + } + + @Override + public int updateDeviceMapping(IntegrationDeviceMapping mapping) { + if (mapping == null || mapping.getId() == null) { + throw new ServiceException("映射ID不能为空"); + } + checkDeviceMapping(mapping); + mapping.setIdentifierMappingJson(normalizeJson(mapping.getIdentifierMappingJson(), "指标映射")); + mapping.setUpdateTime(new Date()); + return deviceMappingMapper.updateById(mapping); + } + + @Override + public int deleteDeviceMappingByIds(Long[] ids) { + int rows = 0; + if (ids == null) { + return rows; + } + for (Long id : ids) { + if (id == null) { + continue; + } + IntegrationDeviceMapping mapping = new IntegrationDeviceMapping(); + mapping.setId(id); + mapping.setDelFlag("2"); + mapping.setUpdateTime(new Date()); + rows += deviceMappingMapper.updateById(mapping); + } + return rows; + } + + @Override + public List selectLogList(IntegrationLog query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query != null) { + wrapper.eq(query.getPlatformId() != null, IntegrationLog::getPlatformId, query.getPlatformId()); + wrapper.like(StringUtils.isNotBlank(query.getPlatformCode()), IntegrationLog::getPlatformCode, query.getPlatformCode()); + wrapper.like(StringUtils.isNotBlank(query.getFlowCode()), IntegrationLog::getFlowCode, query.getFlowCode()); + wrapper.like(StringUtils.isNotBlank(query.getRequestId()), IntegrationLog::getRequestId, query.getRequestId()); + wrapper.eq(query.getStatus() != null, IntegrationLog::getStatus, query.getStatus()); + } + wrapper.orderByDesc(IntegrationLog::getStartTime); + return logMapper.selectList(wrapper); + } + + @Override + public IntegrationSyncResult sync(IntegrationSyncRequest request) { + if (request == null || StringUtils.isBlank(request.getPlatformCode())) { + throw new ServiceException("平台编码不能为空"); + } + String platformCode = request.getPlatformCode().toUpperCase(); + IntegrationPlatform platform = selectPlatformByCode(platformCode); + if (platform == null || platform.getStatus() == null || platform.getStatus() != 1) { + throw new ServiceException("平台不存在或未启用:" + platformCode); + } + PlatformAdapter adapter = adapterMap.get(platformCode); + if (adapter == null) { + throw new ServiceException("未找到平台适配器:" + platformCode); + } + + String requestId = UUID.randomUUID().toString().replace("-", ""); + IntegrationLog log = new IntegrationLog(); + log.setPlatformId(platform.getId()); + log.setPlatformCode(platformCode); + log.setFlowCode(platformCode + "_SYNC"); + log.setRequestId(requestId); + log.setStartTime(new Date()); + log.setStatus(2); + log.setSuccessCount(0); + log.setFailCount(0); + logMapper.insert(log); + + try { + IntegrationSyncResult result = adapter.sync(platform, request); + result.setRequestId(requestId); + log.setStatus(result.getFailCount() == 0 ? 1 : 0); + log.setSuccessCount(result.getSuccessCount()); + log.setFailCount(result.getFailCount()); + log.setDetailJson(com.alibaba.fastjson2.JSON.toJSONString(result.getDetails())); + log.setEndTime(new Date()); + logMapper.updateById(log); + return result; + } catch (Exception e) { + log.setStatus(0); + log.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 1000)); + log.setEndTime(new Date()); + logMapper.updateById(log); + throw e; + } + } + + @Override + public IntegrationSyncResult selfTest(String platformCode) { + String code = StringUtils.isBlank(platformCode) ? "DCP" : platformCode.toUpperCase(); + PlatformAdapter adapter = adapterMap.get(code); + if (adapter == null) { + throw new ServiceException("未找到平台适配器:" + code); + } + IntegrationPlatform platform = selectPlatformByCode(code); + if (platform == null) { + platform = new IntegrationPlatform(); + platform.setPlatformCode(code); + platform.setPlatformName(code + " 自测平台"); + platform.setBaseUrl("http://example.com"); + platform.setStatus(1); + } + IntegrationSyncResult result = adapter.selfTest(platform); + result.setRequestId(UUID.randomUUID().toString().replace("-", "")); + return result; + } + + private void checkPlatform(IntegrationPlatform platform) { + if (platform == null) { + throw new ServiceException("平台配置不能为空"); + } + if (StringUtils.isBlank(platform.getPlatformCode())) { + throw new ServiceException("平台编码不能为空"); + } + if (StringUtils.isBlank(platform.getPlatformName())) { + throw new ServiceException("平台名称不能为空"); + } + if (StringUtils.isBlank(platform.getBaseUrl())) { + throw new ServiceException("平台基础地址不能为空"); + } + } + + private void checkApi(IntegrationApi api) { + if (api == null) { + throw new ServiceException("接口配置不能为空"); + } + if (api.getPlatformId() == null) { + throw new ServiceException("平台ID不能为空"); + } + if (StringUtils.isBlank(api.getApiCode())) { + throw new ServiceException("接口编码不能为空"); + } + if (StringUtils.isBlank(api.getMethod())) { + throw new ServiceException("请求方法不能为空"); + } + if (StringUtils.isBlank(api.getUri())) { + throw new ServiceException("接口URI不能为空"); + } + } + + private void checkDeviceMapping(IntegrationDeviceMapping mapping) { + if (mapping == null) { + throw new ServiceException("设备映射不能为空"); + } + if (mapping.getPlatformId() == null) { + throw new ServiceException("平台ID不能为空"); + } + if (StringUtils.isBlank(mapping.getExternalDeviceId())) { + throw new ServiceException("第三方设备ID不能为空"); + } + if (StringUtils.isBlank(mapping.getSerialNumber())) { + throw new ServiceException("本系统设备编号不能为空"); + } + } + + private String normalizeJson(String json, String label) { + if (StringUtils.isBlank(json)) { + return null; + } + String normalized = json.trim(); + try { + com.alibaba.fastjson2.JSON.parse(normalized); + return normalized; + } catch (Exception e) { + throw new ServiceException(label + "必须是合法JSON"); + } + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpOpenApiServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpOpenApiServiceImpl.java new file mode 100644 index 00000000..f7485a20 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpOpenApiServiceImpl.java @@ -0,0 +1,117 @@ +package com.fastbee.iot.service.impl.integration.dcp; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.service.integration.IDcpOpenApiService; +import com.fastbee.iot.service.integration.IIntegrationPlatformService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * DCP兼容开放接口服务。 + */ +@Service +public class DcpOpenApiServiceImpl implements IDcpOpenApiService { + + private static final String PLATFORM_CODE = "DCP"; + private static final String TOKEN_PREFIX = "iot:integration:open:dcp:token:"; + private static final int TOKEN_SECONDS = 7200; + + @Resource + private IIntegrationPlatformService integrationPlatformService; + + @Resource + private DcpPlatformAdapter dcpPlatformAdapter; + + @Resource + private RedisCache redisCache; + + @Override + public JSONObject createToken(Map form) { + IntegrationPlatform platform = requirePlatform(); + JSONObject credential = StringUtils.isBlank(platform.getCredentialJson()) ? new JSONObject() : JSON.parseObject(platform.getCredentialJson()); + String expectedClientId = credential.getString("openClientId"); + String expectedUserName = credential.getString("openUserName"); + String expectedPassword = credential.getString("openPassword"); + if (StringUtils.isAnyBlank(expectedClientId, expectedUserName, expectedPassword)) { + throw new ServiceException("DCP开放接口账号未配置"); + } + if (!"password".equals(form.get("grant_type"))) { + throw new ServiceException("grant_type不支持"); + } + if (!expectedClientId.equals(form.get("client_id")) + || !expectedUserName.equals(form.get("userName")) + || !expectedPassword.equals(form.get("password"))) { + throw new ServiceException("DCP开放接口账号或密码错误"); + } + String token = UUID.randomUUID().toString().replace("-", ""); + redisCache.setCacheObject(TOKEN_PREFIX + token, platform.getId(), TOKEN_SECONDS, TimeUnit.SECONDS); + + JSONObject result = new JSONObject(); + result.put("access_token", token); + result.put("expires_in", TOKEN_SECONDS); + result.put("token_type", "Bearer"); + result.put("scope", StringUtils.defaultIfBlank(form.get("scope"), "DACP")); + return result; + } + + @Override + public JSONObject getUser(String authorization) { + return dcpPlatformAdapter.fetchUser(requireAuthorizedPlatform(authorization)); + } + + @Override + public JSONArray getProjects(String authorization, String userId) { + if (StringUtils.isBlank(userId)) { + throw new ServiceException("userId不能为空"); + } + return dcpPlatformAdapter.fetchProjects(requireAuthorizedPlatform(authorization), userId); + } + + @Override + public JSONArray getStressSensors(String authorization, String projectId) { + if (StringUtils.isBlank(projectId)) { + throw new ServiceException("projectId不能为空"); + } + return dcpPlatformAdapter.fetchStressSensors(requireAuthorizedPlatform(authorization), projectId); + } + + @Override + public JSONObject getDataset(String authorization, String startTime, String endTime, Boolean original, List deviceIds) { + if (deviceIds == null || deviceIds.isEmpty()) { + throw new ServiceException("deviceId数组不能为空"); + } + return dcpPlatformAdapter.fetchDataset(requireAuthorizedPlatform(authorization), startTime, endTime, original, deviceIds); + } + + private IntegrationPlatform requireAuthorizedPlatform(String authorization) { + if (StringUtils.isBlank(authorization) || !authorization.startsWith("Bearer ")) { + throw new ServiceException("Authorization不能为空"); + } + String token = authorization.substring("Bearer ".length()); + Long platformId = redisCache.getCacheObject(TOKEN_PREFIX + token); + IntegrationPlatform platform = requirePlatform(); + if (platformId == null || !platform.getId().equals(platformId)) { + throw new ServiceException("Authorization无效或已过期"); + } + return platform; + } + + private IntegrationPlatform requirePlatform() { + IntegrationPlatform platform = integrationPlatformService.selectPlatformByCode(PLATFORM_CODE); + if (platform == null || platform.getStatus() == null || platform.getStatus() != 1) { + throw new ServiceException("DCP平台未启用"); + } + return platform; + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpPlatformAdapter.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpPlatformAdapter.java new file mode 100644 index 00000000..67c6ef8c --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/integration/dcp/DcpPlatformAdapter.java @@ -0,0 +1,547 @@ +package com.fastbee.iot.service.impl.integration.dcp; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; +import com.fastbee.common.core.thingsModel.ThingsModelValuesInput; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.domain.integration.IntegrationApi; +import com.fastbee.iot.domain.integration.IntegrationDeviceMapping; +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.mapper.integration.IntegrationApiMapper; +import com.fastbee.iot.mapper.integration.IntegrationDeviceMappingMapper; +import com.fastbee.iot.model.integration.IntegrationSyncRequest; +import com.fastbee.iot.model.integration.IntegrationSyncResult; +import com.fastbee.iot.model.integration.UnifiedTelemetry; +import com.fastbee.iot.service.IDeviceService; +import com.fastbee.iot.service.integration.PlatformAdapter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * DCP 轴力计平台适配器。 + */ +@Slf4j +@Component +public class DcpPlatformAdapter implements PlatformAdapter { + + private static final String PLATFORM_CODE = "DCP"; + private static final String API_TOKEN = "dcp_token"; + private static final String API_USER = "dcp_user_id"; + private static final String API_PROJECT = "dcp_project_list"; + private static final String API_DEVICE = "dcp_stress_sensor_list"; + private static final String API_DATA = "dcp_stress_sensor_data"; + + @Resource + private IntegrationApiMapper apiMapper; + + @Resource + private IntegrationDeviceMappingMapper deviceMappingMapper; + + @Resource + private IDeviceService deviceService; + + @Resource + private RedisCache redisCache; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public String platformCode() { + return PLATFORM_CODE; + } + + @Override + public IntegrationSyncResult sync(IntegrationPlatform platform, IntegrationSyncRequest request) { + IntegrationSyncResult result = new IntegrationSyncResult(); + result.setPlatformCode(PLATFORM_CODE); + result.setFlowCode("DCP_STRESS_SENSOR_SYNC"); + result.setDryRun(Boolean.TRUE.equals(request.getDryRun())); + + JSONObject credential = parseObject(platform.getCredentialJson()); + String authorization = resolveAuthorization(platform, credential); + String userId = getUserId(platform, authorization); + String projectId = StringUtils.isNotBlank(request.getProjectId()) ? request.getProjectId() : getFirstProjectId(platform, authorization, userId); + if (StringUtils.isBlank(projectId)) { + throw new ServiceException("DCP项目ID为空"); + } + + List devices = getDevices(platform, authorization, projectId); + List targetDeviceIds = selectTargetDeviceIds(devices, request.getExternalDeviceIds()); + result.setDeviceCount(targetDeviceIds.size()); + if (targetDeviceIds.isEmpty()) { + result.setMessage("未获取到可同步的DCP轴力计测点"); + return result; + } + + Map> dataMap = getDataset(platform, authorization, request, targetDeviceIds); + Map mappings = loadMappings(platform.getId(), targetDeviceIds); + + int dataCount = 0; + for (String externalDeviceId : targetDeviceIds) { + List rows = dataMap.get(externalDeviceId); + if (rows == null) { + continue; + } + IntegrationDeviceMapping mapping = mappings.get(externalDeviceId); + for (JSONObject row : rows) { + dataCount++; + if (mapping == null) { + result.addFail("未配置设备映射 externalDeviceId=" + externalDeviceId); + continue; + } + try { + UnifiedTelemetry telemetry = convertTelemetry(externalDeviceId, findDeviceName(devices, externalDeviceId), row); + if (!result.isDryRun()) { + writeThingsModel(mapping, telemetry); + } + result.addSuccess(); + } catch (Exception e) { + result.addFail("写入失败 externalDeviceId=" + externalDeviceId + ", error=" + e.getMessage()); + } + } + } + result.setDataCount(dataCount); + result.setMessage(result.isDryRun() ? "DCP同步演练完成" : "DCP同步完成"); + return result; + } + + @Override + public IntegrationSyncResult selfTest(IntegrationPlatform platform) { + IntegrationSyncResult result = new IntegrationSyncResult(); + result.setPlatformCode(PLATFORM_CODE); + result.setFlowCode("DCP_STRESS_SENSOR_SELF_TEST"); + result.setDryRun(true); + + JSONObject sample = JSON.parseObject("{\"isDeleted\":false,\"epoch\":\"2026-04-09T08:05:12+08:00\",\"deviceId\":\"3a1e1d88-c0ed-5f77-56f1-7704351b5a33\",\"e\":-323.26,\"fi\":0,\"ti\":0,\"span\":0,\"isOriginal\":true,\"temperature\":0,\"ct\":0,\"pt\":0}"); + UnifiedTelemetry telemetry = convertTelemetry(sample.getString("deviceId"), "第二层支护结构DK138+931", sample); + if (!"3a1e1d88-c0ed-5f77-56f1-7704351b5a33".equals(telemetry.getExternalDeviceId())) { + result.addFail("设备ID解析失败"); + } + if (!telemetry.getValues().containsKey("e") || !telemetry.getValues().containsKey("temperature")) { + result.addFail("轴力计数据字段解析失败"); + } + if (telemetry.getReportTime() == null) { + result.addFail("数据时间解析失败"); + } + if (result.getFailCount() == 0) { + result.addSuccess(); + result.setDataCount(1); + result.setDeviceCount(1); + result.setMessage("DCP适配器自测通过"); + } else { + result.setMessage("DCP适配器自测失败"); + } + return result; + } + + public JSONObject fetchUser(IntegrationPlatform platform) { + String authorization = resolveAuthorization(platform, parseObject(platform.getCredentialJson())); + IntegrationApi api = getApi(platform.getId(), API_USER); + return exchangeJson(platform, api, authorization, null, null); + } + + public JSONArray fetchProjects(IntegrationPlatform platform, String userId) { + String authorization = resolveAuthorization(platform, parseObject(platform.getCredentialJson())); + IntegrationApi api = getApi(platform.getId(), API_PROJECT); + Map pathVars = new HashMap<>(); + pathVars.put("userId", userId); + return exchangeArray(platform, api, authorization, pathVars, null); + } + + public JSONArray fetchStressSensors(IntegrationPlatform platform, String projectId) { + String authorization = resolveAuthorization(platform, parseObject(platform.getCredentialJson())); + IntegrationApi api = getApi(platform.getId(), API_DEVICE); + Map pathVars = new HashMap<>(); + pathVars.put("projectId", projectId); + pathVars.put("projcetId", projectId); + return exchangeArray(platform, api, authorization, pathVars, null); + } + + public JSONObject fetchDataset(IntegrationPlatform platform, String startTime, String endTime, Boolean original, List deviceIds) { + String authorization = resolveAuthorization(platform, parseObject(platform.getCredentialJson())); + IntegrationApi api = getApi(platform.getId(), API_DATA); + Map query = new HashMap<>(); + query.put("startTime", defaultStartTime(startTime)); + query.put("endTime", defaultEndTime(endTime)); + query.put("isOriginal", String.valueOf(original == null || original)); + return exchangeJson(platform, api, authorization, query, deviceIds == null ? Collections.emptyList() : deviceIds); + } + + private String resolveAuthorization(IntegrationPlatform platform, JSONObject credential) { + String cacheKey = "iot:integration:dcp:token:" + platform.getId(); + try { + String cached = redisCache.getCacheObject(cacheKey); + if (StringUtils.isNotBlank(cached)) { + return cached; + } + } catch (Exception e) { + log.debug("读取DCP token缓存失败,将直接请求token:{}", e.getMessage()); + } + + IntegrationApi api = getApi(platform.getId(), API_TOKEN); + String url = buildUrl(platform.getBaseUrl(), api.getUri()); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", value(credential, "grant_type", "grantType", "password")); + body.add("scope", value(credential, "scope", null, "DACP")); + body.add("client_id", value(credential, "client_id", "clientId", "DACP_App")); + body.add("userName", value(credential, "userName", "username", null)); + body.add("password", value(credential, "password", null, null)); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, headers), String.class); + JSONObject json = JSON.parseObject(response.getBody()); + String token = json.getString("access_token"); + String tokenType = json.getString("token_type"); + if (StringUtils.isBlank(token)) { + throw new ServiceException("DCP token获取失败"); + } + String authorization = (StringUtils.isBlank(tokenType) ? "Bearer" : tokenType) + " " + token; + int expires = json.getIntValue("expires_in"); + try { + redisCache.setCacheObject(cacheKey, authorization, Math.max(expires - 60, 60), TimeUnit.SECONDS); + } catch (Exception e) { + log.debug("写入DCP token缓存失败:{}", e.getMessage()); + } + return authorization; + } + + private String getUserId(IntegrationPlatform platform, String authorization) { + IntegrationApi api = getApi(platform.getId(), API_USER); + JSONObject json = exchangeJson(platform, api, authorization, null, null); + String id = json.getString("id"); + if (StringUtils.isBlank(id)) { + throw new ServiceException("DCP用户ID获取失败"); + } + return id; + } + + private String getFirstProjectId(IntegrationPlatform platform, String authorization, String userId) { + IntegrationApi api = getApi(platform.getId(), API_PROJECT); + Map pathVars = new HashMap<>(); + pathVars.put("userId", userId); + JSONArray array = exchangeArray(platform, api, authorization, pathVars, null); + if (array == null || array.isEmpty()) { + return null; + } + return array.getJSONObject(0).getString("id"); + } + + private List getDevices(IntegrationPlatform platform, String authorization, String projectId) { + IntegrationApi api = getApi(platform.getId(), API_DEVICE); + Map pathVars = new HashMap<>(); + pathVars.put("projectId", projectId); + pathVars.put("projcetId", projectId); + JSONArray array = exchangeArray(platform, api, authorization, pathVars, null); + return toObjectList(array); + } + + private Map> getDataset(IntegrationPlatform platform, String authorization, IntegrationSyncRequest request, List deviceIds) { + IntegrationApi api = getApi(platform.getId(), API_DATA); + Map query = new HashMap<>(); + query.put("startTime", defaultStartTime(request.getStartTime())); + query.put("endTime", defaultEndTime(request.getEndTime())); + query.put("isOriginal", String.valueOf(request.getOriginal() == null || request.getOriginal())); + JSONObject json = exchangeJson(platform, api, authorization, query, deviceIds); + Map> result = new LinkedHashMap<>(); + for (String key : json.keySet()) { + result.put(key, toObjectList(json.getJSONArray(key))); + } + return result; + } + + private JSONObject exchangeJson(IntegrationPlatform platform, IntegrationApi api, String authorization, Map varsOrQuery, Object body) { + String response = exchange(platform, api, authorization, varsOrQuery, body); + if (StringUtils.isBlank(response)) { + return new JSONObject(); + } + return JSON.parseObject(response); + } + + private JSONArray exchangeArray(IntegrationPlatform platform, IntegrationApi api, String authorization, Map varsOrQuery, Object body) { + String response = exchange(platform, api, authorization, varsOrQuery, body); + if (StringUtils.isBlank(response)) { + return new JSONArray(); + } + return JSON.parseArray(response); + } + + private String exchange(IntegrationPlatform platform, IntegrationApi api, String authorization, Map varsOrQuery, Object body) { + String uri = render(api.getUri(), varsOrQuery); + String url = buildUrl(platform.getBaseUrl(), uri); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authorization); + if ("application/x-www-form-urlencoded".equalsIgnoreCase(api.getContentType())) { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + } else { + headers.setContentType(MediaType.APPLICATION_JSON); + } + if ("GET".equalsIgnoreCase(api.getMethod())) { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class); + return response.getBody(); + } + if (API_DATA.equals(api.getApiCode()) && varsOrQuery != null && !varsOrQuery.isEmpty()) { + url = appendQuery(url, varsOrQuery); + } + HttpMethod method = HttpMethod.resolve(api.getMethod()); + if (method == null) { + throw new ServiceException("不支持的请求方法:" + api.getMethod()); + } + ResponseEntity response = restTemplate.exchange(url, method, new HttpEntity<>(body, headers), String.class); + return response.getBody(); + } + + private void writeThingsModel(IntegrationDeviceMapping mapping, UnifiedTelemetry telemetry) { + Map identifierMapping = parseStringMap(mapping.getIdentifierMappingJson()); + if (identifierMapping.isEmpty()) { + identifierMapping.put("e", "axialForce"); + identifierMapping.put("temperature", "temperature"); + identifierMapping.put("ct", "ct"); + identifierMapping.put("pt", "pt"); + } + List items = new ArrayList<>(); + for (Map.Entry entry : telemetry.getValues().entrySet()) { + String identifier = identifierMapping.get(entry.getKey()); + if (StringUtils.isBlank(identifier) || entry.getValue() == null) { + continue; + } + ThingsModelSimpleItem item = new ThingsModelSimpleItem(); + item.setId(identifier); + item.setValue(String.valueOf(entry.getValue())); + item.setTs(telemetry.getReportTime()); + item.setRemark("integration:" + PLATFORM_CODE); + items.add(item); + } + if (items.isEmpty()) { + return; + } + ThingsModelValuesInput input = new ThingsModelValuesInput(); + input.setProductId(mapping.getProductId()); + input.setDeviceNumber(mapping.getSerialNumber()); + input.setThingsModelValueRemarkItem(items); + deviceService.reportDeviceThingsModelValue(input, 1, false); + } + + private UnifiedTelemetry convertTelemetry(String externalDeviceId, String externalDeviceName, JSONObject row) { + UnifiedTelemetry telemetry = new UnifiedTelemetry(); + telemetry.setPlatformCode(PLATFORM_CODE); + telemetry.setExternalDeviceId(externalDeviceId); + telemetry.setExternalDeviceName(externalDeviceName); + telemetry.setRaw(row); + telemetry.setReportTime(parseDate(row.getString("epoch"))); + putIfPresent(telemetry.getValues(), "e", row.get("e")); + putIfPresent(telemetry.getValues(), "temperature", row.get("temperature")); + putIfPresent(telemetry.getValues(), "ct", row.get("ct")); + putIfPresent(telemetry.getValues(), "pt", row.get("pt")); + putIfPresent(telemetry.getValues(), "fi", row.get("fi")); + putIfPresent(telemetry.getValues(), "ti", row.get("ti")); + putIfPresent(telemetry.getValues(), "span", row.get("span")); + return telemetry; + } + + private Map loadMappings(Long platformId, List externalDeviceIds) { + if (externalDeviceIds.isEmpty()) { + return Collections.emptyMap(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationDeviceMapping::getPlatformId, platformId) + .eq(IntegrationDeviceMapping::getStatus, 1) + .eq(IntegrationDeviceMapping::getDelFlag, "0") + .in(IntegrationDeviceMapping::getExternalDeviceId, externalDeviceIds); + List list = deviceMappingMapper.selectList(wrapper); + Map result = new HashMap<>(); + for (IntegrationDeviceMapping item : list) { + result.put(item.getExternalDeviceId(), item); + } + return result; + } + + private IntegrationApi getApi(Long platformId, String apiCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(IntegrationApi::getPlatformId, platformId) + .eq(IntegrationApi::getApiCode, apiCode) + .eq(IntegrationApi::getDelFlag, "0") + .last("limit 1"); + IntegrationApi api = apiMapper.selectOne(wrapper); + return api == null ? defaultApi(apiCode) : api; + } + + private IntegrationApi defaultApi(String apiCode) { + IntegrationApi api = new IntegrationApi(); + api.setApiCode(apiCode); + api.setStatus(1); + api.setContentType(MediaType.APPLICATION_JSON_VALUE); + if (API_TOKEN.equals(apiCode)) { + api.setMethod("POST"); + api.setUri("/connect/token"); + api.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } else if (API_USER.equals(apiCode)) { + api.setMethod("GET"); + api.setUri("/api/app/user/user-id"); + } else if (API_PROJECT.equals(apiCode)) { + api.setMethod("GET"); + api.setUri("/api/app/project/by-user-id/{userId}"); + } else if (API_DEVICE.equals(apiCode)) { + api.setMethod("POST"); + api.setUri("/api/app/stress-sensor/by-project-id/{projectId}"); + } else if (API_DATA.equals(apiCode)) { + api.setMethod("POST"); + api.setUri("/api/app/inclino-data/load-dataset-by-device-ids"); + } else { + throw new ServiceException("未知DCP接口编码:" + apiCode); + } + return api; + } + + private List selectTargetDeviceIds(List devices, List requested) { + if (requested != null && !requested.isEmpty()) { + return requested; + } + List ids = new ArrayList<>(); + for (JSONObject device : devices) { + String id = device.getString("id"); + if (StringUtils.isNotBlank(id)) { + ids.add(id); + } + } + return ids; + } + + private String findDeviceName(List devices, String id) { + for (JSONObject device : devices) { + if (id.equals(device.getString("id"))) { + return device.getString("name"); + } + } + return null; + } + + private List toObjectList(JSONArray array) { + List result = new ArrayList<>(); + if (array == null) { + return result; + } + for (int i = 0; i < array.size(); i++) { + result.add(array.getJSONObject(i)); + } + return result; + } + + private JSONObject parseObject(String json) { + return StringUtils.isBlank(json) ? new JSONObject() : JSON.parseObject(json); + } + + private Map parseStringMap(String json) { + Map result = new LinkedHashMap<>(); + if (StringUtils.isBlank(json)) { + return result; + } + JSONObject object = JSON.parseObject(json); + for (String key : object.keySet()) { + result.put(key, object.getString(key)); + } + return result; + } + + private String value(JSONObject object, String key, String alias, String defaultValue) { + String value = object.getString(key); + if (StringUtils.isBlank(value) && StringUtils.isNotBlank(alias)) { + value = object.getString(alias); + } + return StringUtils.isBlank(value) ? defaultValue : value; + } + + private Date parseDate(String text) { + if (StringUtils.isBlank(text)) { + return new Date(); + } + return Date.from(OffsetDateTime.parse(text).toInstant()); + } + + private void putIfPresent(Map values, String key, Object value) { + if (value == null) { + return; + } + if (value instanceof BigDecimal) { + values.put(key, ((BigDecimal) value).stripTrailingZeros().toPlainString()); + } else { + values.put(key, value); + } + } + + private String defaultStartTime(String value) { + if (StringUtils.isNotBlank(value)) { + return value; + } + return OffsetDateTime.now().minusDays(1).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + private String defaultEndTime(String value) { + if (StringUtils.isNotBlank(value)) { + return value; + } + return OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + private String buildUrl(String baseUrl, String uri) { + String base = baseUrl == null ? "" : baseUrl; + while (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + String path = uri.startsWith("/") ? uri : "/" + uri; + return base + path; + } + + private String render(String template, Map vars) { + if (template == null || vars == null) { + return template; + } + String result = template; + for (Map.Entry entry : vars.entrySet()) { + String value = String.valueOf(entry.getValue()); + result = result.replace("{" + entry.getKey() + "}", value); + result = result.replace("${" + entry.getKey() + "}", value); + } + return result; + } + + private String appendQuery(String url, Map query) { + StringBuilder builder = new StringBuilder(url); + builder.append(url.contains("?") ? "&" : "?"); + boolean first = true; + for (Map.Entry entry : query.entrySet()) { + if (!first) { + builder.append("&"); + } + builder.append(encode(entry.getKey())).append("=").append(encode(String.valueOf(entry.getValue()))); + first = false; + } + return builder.toString(); + } + + private String encode(String value) { + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ServiceException("URL参数编码失败"); + } + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IDcpOpenApiService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IDcpOpenApiService.java new file mode 100644 index 00000000..aed25a22 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IDcpOpenApiService.java @@ -0,0 +1,20 @@ +package com.fastbee.iot.service.integration; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; + +import java.util.List; +import java.util.Map; + +public interface IDcpOpenApiService { + + JSONObject createToken(Map form); + + JSONObject getUser(String authorization); + + JSONArray getProjects(String authorization, String userId); + + JSONArray getStressSensors(String authorization, String projectId); + + JSONObject getDataset(String authorization, String startTime, String endTime, Boolean original, List deviceIds); +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IIntegrationPlatformService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IIntegrationPlatformService.java new file mode 100644 index 00000000..b043c7f6 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/IIntegrationPlatformService.java @@ -0,0 +1,51 @@ +package com.fastbee.iot.service.integration; + +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.domain.integration.IntegrationApi; +import com.fastbee.iot.domain.integration.IntegrationDeviceMapping; +import com.fastbee.iot.domain.integration.IntegrationLog; +import com.fastbee.iot.model.integration.IntegrationSyncRequest; +import com.fastbee.iot.model.integration.IntegrationSyncResult; + +import java.util.List; + +public interface IIntegrationPlatformService { + + IntegrationPlatform selectPlatformByCode(String platformCode); + + IntegrationPlatform selectPlatformById(Long id); + + List selectPlatformList(IntegrationPlatform query); + + int insertPlatform(IntegrationPlatform platform); + + int updatePlatform(IntegrationPlatform platform); + + int deletePlatformByIds(Long[] ids); + + List selectApiList(IntegrationApi query); + + IntegrationApi selectApiById(Long id); + + int insertApi(IntegrationApi api); + + int updateApi(IntegrationApi api); + + int deleteApiByIds(Long[] ids); + + List selectDeviceMappingList(IntegrationDeviceMapping query); + + IntegrationDeviceMapping selectDeviceMappingById(Long id); + + int insertDeviceMapping(IntegrationDeviceMapping mapping); + + int updateDeviceMapping(IntegrationDeviceMapping mapping); + + int deleteDeviceMappingByIds(Long[] ids); + + List selectLogList(IntegrationLog query); + + IntegrationSyncResult sync(IntegrationSyncRequest request); + + IntegrationSyncResult selfTest(String platformCode); +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/PlatformAdapter.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/PlatformAdapter.java new file mode 100644 index 00000000..4eafd3e0 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/integration/PlatformAdapter.java @@ -0,0 +1,14 @@ +package com.fastbee.iot.service.integration; + +import com.fastbee.iot.domain.integration.IntegrationPlatform; +import com.fastbee.iot.model.integration.IntegrationSyncRequest; +import com.fastbee.iot.model.integration.IntegrationSyncResult; + +public interface PlatformAdapter { + + String platformCode(); + + IntegrationSyncResult sync(IntegrationPlatform platform, IntegrationSyncRequest request); + + IntegrationSyncResult selfTest(IntegrationPlatform platform); +} diff --git a/sql/iot_integration_platform.sql b/sql/iot_integration_platform.sql new file mode 100644 index 00000000..b7716315 --- /dev/null +++ b/sql/iot_integration_platform.sql @@ -0,0 +1,170 @@ +-- 第三方平台接入框架 + +CREATE TABLE IF NOT EXISTS `iot_integration_platform` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `platform_code` varchar(64) NOT NULL COMMENT '平台编码', + `platform_name` varchar(128) NOT NULL COMMENT '平台名称', + `base_url` varchar(512) NOT NULL COMMENT '基础地址', + `auth_type` varchar(64) DEFAULT NULL COMMENT '认证类型', + `credential_json` json DEFAULT NULL COMMENT '凭证配置', + `status` tinyint DEFAULT 1 COMMENT '状态:0=停用 1=启用', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_integration_platform_code` (`platform_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方平台接入配置'; + +CREATE TABLE IF NOT EXISTS `iot_integration_api` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `platform_id` bigint NOT NULL COMMENT '平台ID', + `api_code` varchar(64) NOT NULL COMMENT '接口编码', + `api_name` varchar(128) NOT NULL COMMENT '接口名称', + `method` varchar(16) NOT NULL COMMENT '请求方法', + `uri` varchar(512) NOT NULL COMMENT '接口URI', + `content_type` varchar(128) DEFAULT 'application/json' COMMENT 'Content-Type', + `headers_json` json DEFAULT NULL COMMENT '请求头模板', + `query_json` json DEFAULT NULL COMMENT 'Query模板', + `body_json` json DEFAULT NULL COMMENT 'Body模板', + `response_path` varchar(256) DEFAULT NULL COMMENT '返回路径', + `depends_on` varchar(64) DEFAULT NULL COMMENT '依赖接口编码', + `sort_order` int DEFAULT 0 COMMENT '排序', + `status` tinyint DEFAULT 1 COMMENT '状态:0=停用 1=启用', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_integration_api_code` (`platform_id`, `api_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方平台接口定义'; + +CREATE TABLE IF NOT EXISTS `iot_integration_device_mapping` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `platform_id` bigint NOT NULL COMMENT '平台ID', + `external_device_id` varchar(128) NOT NULL COMMENT '第三方设备ID', + `external_device_name` varchar(128) DEFAULT NULL COMMENT '第三方设备名称', + `product_id` bigint NOT NULL COMMENT '本系统产品ID', + `serial_number` varchar(64) NOT NULL COMMENT '本系统设备编号', + `identifier_mapping_json` json DEFAULT NULL COMMENT '指标映射 sourceField -> identifier', + `status` tinyint DEFAULT 1 COMMENT '状态:0=停用 1=启用', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_integration_device_mapping` (`platform_id`, `external_device_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方设备映射'; + +CREATE TABLE IF NOT EXISTS `iot_integration_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `platform_id` bigint DEFAULT NULL COMMENT '平台ID', + `platform_code` varchar(64) NOT NULL COMMENT '平台编码', + `flow_code` varchar(64) NOT NULL COMMENT '流程编码', + `request_id` varchar(64) NOT NULL COMMENT '请求ID', + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `end_time` datetime DEFAULT NULL COMMENT '结束时间', + `status` tinyint DEFAULT 2 COMMENT '状态:0=失败 1=成功 2=执行中', + `success_count` int DEFAULT 0 COMMENT '成功数量', + `fail_count` int DEFAULT 0 COMMENT '失败数量', + `error_msg` varchar(1000) DEFAULT NULL COMMENT '错误信息', + `detail_json` json DEFAULT NULL COMMENT '详情', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_integration_log_request` (`request_id`), + KEY `idx_integration_log_platform` (`platform_code`, `start_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方平台接入执行日志'; + +INSERT INTO `iot_integration_platform` +(`platform_code`, `platform_name`, `base_url`, `auth_type`, `credential_json`, `status`, `del_flag`, `create_by`, `create_time`) +VALUES +('DCP', 'DCP轴力计平台', 'http://47.108.182.132:8200', 'PASSWORD_TOKEN', + JSON_OBJECT('grant_type','password','scope','DACP','client_id','DACP_App','userName','ceshi','password','12345678', + 'openClientId','DCP_Open_App','openUserName','open_user','openPassword','change-me'), + 0, '0', 'admin', NOW()) +ON DUPLICATE KEY UPDATE `platform_name` = VALUES(`platform_name`); + +-- 启用前请按现场账号修改 credential_json 中的 DCP 上游账号和 openClientId/openUserName/openPassword,并将 status 改为 1。 +-- 若需要第三方应用匿名触发同步,请在 credential_json 中增加 openApiKey: +-- UPDATE iot_integration_platform SET credential_json = JSON_SET(credential_json, '$.openApiKey', 'replace-with-random-secret') WHERE platform_code = 'DCP'; +-- +-- 设备映射示例,需替换 product_id、serial_number、external_device_id: +-- INSERT INTO iot_integration_device_mapping +-- (platform_id, external_device_id, external_device_name, product_id, serial_number, identifier_mapping_json, status, del_flag, create_by, create_time) +-- SELECT id, 'DCP_DEVICE_ID', 'DCP测点名称', 1, 'FASTBEE_SERIAL_NUMBER', +-- JSON_OBJECT('e','axialForce','temperature','temperature','ct','ct','pt','pt'), +-- 1, '0', 'admin', NOW() +-- FROM iot_integration_platform WHERE platform_code = 'DCP'; + +INSERT INTO `iot_integration_api` +(`platform_id`, `api_code`, `api_name`, `method`, `uri`, `content_type`, `depends_on`, `sort_order`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT p.id, 'dcp_token', '获取访问凭证', 'POST', '/connect/token', 'application/x-www-form-urlencoded', NULL, 1, 1, '0', 'admin', NOW() +FROM `iot_integration_platform` p WHERE p.platform_code = 'DCP' +ON DUPLICATE KEY UPDATE `uri` = VALUES(`uri`); + +INSERT INTO `iot_integration_api` +(`platform_id`, `api_code`, `api_name`, `method`, `uri`, `content_type`, `depends_on`, `sort_order`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT p.id, 'dcp_user_id', '获取用户信息', 'GET', '/api/app/user/user-id', 'application/json', 'dcp_token', 2, 1, '0', 'admin', NOW() +FROM `iot_integration_platform` p WHERE p.platform_code = 'DCP' +ON DUPLICATE KEY UPDATE `uri` = VALUES(`uri`); + +INSERT INTO `iot_integration_api` +(`platform_id`, `api_code`, `api_name`, `method`, `uri`, `content_type`, `depends_on`, `sort_order`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT p.id, 'dcp_project_list', '获取项目信息', 'GET', '/api/app/project/by-user-id/{userId}', 'application/json', 'dcp_user_id', 3, 1, '0', 'admin', NOW() +FROM `iot_integration_platform` p WHERE p.platform_code = 'DCP' +ON DUPLICATE KEY UPDATE `uri` = VALUES(`uri`); + +INSERT INTO `iot_integration_api` +(`platform_id`, `api_code`, `api_name`, `method`, `uri`, `content_type`, `depends_on`, `sort_order`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT p.id, 'dcp_stress_sensor_list', '获取项目轴力计测点', 'POST', '/api/app/stress-sensor/by-project-id/{projectId}', 'application/json', 'dcp_project_list', 4, 1, '0', 'admin', NOW() +FROM `iot_integration_platform` p WHERE p.platform_code = 'DCP' +ON DUPLICATE KEY UPDATE `uri` = VALUES(`uri`); + +INSERT INTO `iot_integration_api` +(`platform_id`, `api_code`, `api_name`, `method`, `uri`, `content_type`, `depends_on`, `sort_order`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT p.id, 'dcp_stress_sensor_data', '批量获取轴力计数据', 'POST', '/api/app/inclino-data/load-dataset-by-device-ids', 'application/json', 'dcp_stress_sensor_list', 5, 1, '0', 'admin', NOW() +FROM `iot_integration_platform` p WHERE p.platform_code = 'DCP' +ON DUPLICATE KEY UPDATE `uri` = VALUES(`uri`); + +-- 菜单权限。若目标库已有相同 menu_id,请按现场菜单ID调整后执行。 +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3440, '平台接入', 3051, 9, 'integration', 'iot/integration/index', NULL, 1, 0, 'C', '0', '0', 'iot:integration:list', 'api', 'admin', NOW(), '', NULL, '第三方平台接入菜单') +ON DUPLICATE KEY UPDATE `menu_name` = VALUES(`menu_name`), `component` = VALUES(`component`), `visible` = VALUES(`visible`), `perms` = VALUES(`perms`); + +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3441, '平台接入查询', 3440, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:integration:list', '#', 'admin', NOW(), '', NULL, '') +ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`); + +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3442, '平台数据同步', 3440, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:integration:sync', '#', 'admin', NOW(), '', NULL, '') +ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`); + +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3443, '平台接入新增', 3440, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:integration:add', '#', 'admin', NOW(), '', NULL, '') +ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`); + +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3444, '平台接入修改', 3440, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:integration:edit', '#', 'admin', NOW(), '', NULL, '') +ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`); + +INSERT INTO `sys_menu` +(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES +(3445, '平台接入删除', 3440, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:integration:remove', '#', 'admin', NOW(), '', NULL, '') +ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`); diff --git a/yack-web/src/api/iot/integration.js b/yack-web/src/api/iot/integration.js new file mode 100644 index 00000000..47bb5a10 --- /dev/null +++ b/yack-web/src/api/iot/integration.js @@ -0,0 +1,138 @@ +import request from '@/utils/request' + +export function listPlatform(query) { + return request({ + url: '/iot/integration/platform/list', + method: 'get', + params: query + }) +} + +export function getPlatform(id) { + return request({ + url: '/iot/integration/platform/' + id, + method: 'get' + }) +} + +export function addPlatform(data) { + return request({ + url: '/iot/integration/platform', + method: 'post', + data: data + }) +} + +export function updatePlatform(data) { + return request({ + url: '/iot/integration/platform', + method: 'put', + data: data + }) +} + +export function delPlatform(id) { + return request({ + url: '/iot/integration/platform/' + id, + method: 'delete' + }) +} + +export function listIntegrationApi(query) { + return request({ + url: '/iot/integration/api/list', + method: 'get', + params: query + }) +} + +export function getIntegrationApi(id) { + return request({ + url: '/iot/integration/api/' + id, + method: 'get' + }) +} + +export function addIntegrationApi(data) { + return request({ + url: '/iot/integration/api', + method: 'post', + data: data + }) +} + +export function updateIntegrationApi(data) { + return request({ + url: '/iot/integration/api', + method: 'put', + data: data + }) +} + +export function delIntegrationApi(id) { + return request({ + url: '/iot/integration/api/' + id, + method: 'delete' + }) +} + +export function listDeviceMapping(query) { + return request({ + url: '/iot/integration/device-mapping/list', + method: 'get', + params: query + }) +} + +export function getDeviceMapping(id) { + return request({ + url: '/iot/integration/device-mapping/' + id, + method: 'get' + }) +} + +export function addDeviceMapping(data) { + return request({ + url: '/iot/integration/device-mapping', + method: 'post', + data: data + }) +} + +export function updateDeviceMapping(data) { + return request({ + url: '/iot/integration/device-mapping', + method: 'put', + data: data + }) +} + +export function delDeviceMapping(id) { + return request({ + url: '/iot/integration/device-mapping/' + id, + method: 'delete' + }) +} + +export function listIntegrationLog(query) { + return request({ + url: '/iot/integration/log/list', + method: 'get', + params: query + }) +} + +export function syncIntegration(data) { + return request({ + url: '/iot/integration/sync', + method: 'post', + data: data + }) +} + +export function selfTestIntegration(platformCode) { + return request({ + url: '/iot/integration/self-test/' + platformCode, + method: 'post' + }) +} diff --git a/yack-web/src/views/iot/integration/index.vue b/yack-web/src/views/iot/integration/index.vue new file mode 100644 index 00000000..faa1e769 --- /dev/null +++ b/yack-web/src/views/iot/integration/index.vue @@ -0,0 +1,808 @@ + + + + +