From 7a196bddd5143f40375b7228916c9434ebf42439 Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Fri, 12 Dec 2025 14:06:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=91=E7=9C=8B=E7=9D=80=20iot=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B1=9E=E6=80=A7=E4=BF=9D=E5=AD=98=E5=92=8C=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=A7=84=E5=88=99=E5=8C=B9=E9=85=8D=E6=98=AF=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E6=89=A7=E8=A1=8C=E7=9A=84=EF=BC=8C=20Redis=E4=B8=AD?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E8=BF=98=E6=B2=A1=E6=9C=89=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E6=88=91=E7=8E=B0=E5=9C=A8=E5=B7=B2?= =?UTF-8?q?=E7=BB=8F=E5=AE=9E=E7=8E=B0=E4=BA=86modbus=20rtu/tcp=E7=9A=84?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E8=BD=AE=E8=AF=A2=E5=92=8C=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=20(hex=E6=A0=BC=E5=BC=8F)=E5=92=8C=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=81=94=E5=8A=A8=E4=B8=8B=E5=8F=91=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=EF=BC=8C=E4=BD=86=E6=98=AF=E6=9C=89=E4=B8=AA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=A6=82=E6=9E=9C=E4=BD=BF=E7=94=A8=E8=BF=99=E4=B8=AA?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=A7=84=E5=88=99=E5=8C=B9=E9=85=8D=EF=BC=8C?= =?UTF-8?q?=E9=82=A3=E6=88=91=E5=8F=AA=E8=83=BD=E8=BD=AE=E8=AF=A2=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=89=A9=E6=A8=A1=E5=9E=8B=20=E7=84=B6=E5=90=8E?= =?UTF-8?q?=E6=88=91=E7=8E=B0=E5=9C=A8=E6=98=AF=E8=BF=99=E6=A0=B7=E5=81=9A?= =?UTF-8?q?=E7=9A=84=EF=BC=9A=E8=AE=BE=E5=A4=87=20hex=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=BD=91=E5=85=B3=E8=A7=A3=E7=A0=81=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=BD=91=E5=85=B3=E8=BD=AC=E6=8D=A2=20(=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E7=BB=84=E8=A3=85)=E2=80=94=E2=80=94=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=80=BB=E7=BA=BF=E2=80=94=E2=80=94=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E8=81=94=E5=8A=A8=20=E7=84=B6=E5=90=8E=E5=AF=B9=E4=BA=8E?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=8F=91=E9=80=81=E7=9A=84=20json=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E2=80=94=E2=80=94=E2=80=94=E2=80=94=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E2=80=94=E2=80=94=E5=9C=BA=E6=99=AF=E8=81=94?= =?UTF-8?q?=E5=8A=A8=20=E4=B8=8D=E7=9F=A5=E9=81=93=E8=BF=99=E6=A0=B7?= =?UTF-8?q?=E5=8E=BB=E5=81=9A=E6=98=AF=E5=90=A6=E5=8F=AF=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E8=BF=99=E9=83=A8=E5=88=86=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=8C=E6=B5=8B=E8=AF=95=E5=8F=AF=E8=A1=8C?= =?UTF-8?q?=EF=BC=8C=E6=9A=82=E6=97=B6=E6=B2=A1=E6=9C=89=E7=94=A8=E5=88=B0?= =?UTF-8?q?=E7=94=9F=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/product/product/index.ts | 2 + src/api/iot/rule/data/sink/index.ts | 38 ++ src/api/iot/thingmodel/index.ts | 18 + src/views/iot/product/product/ProductForm.vue | 59 ++- src/views/iot/rule/data/sink/DataSinkForm.vue | 10 +- .../data/sink/config/DatabaseConfigForm.vue | 367 ++++++++++++++++++ .../sink/config/components/KeyValueEditor.vue | 43 +- src/views/iot/rule/data/sink/config/index.ts | 2 + .../iot/thingmodel/ThingModelProperty.vue | 210 +++++++++- .../iot/thingmodel/ThingModelService.vue | 64 +++ src/views/iot/utils/constants.ts | 20 + 11 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 src/views/iot/rule/data/sink/config/DatabaseConfigForm.vue diff --git a/src/api/iot/product/product/index.ts b/src/api/iot/product/product/index.ts index c9f273ebe..c2a047ca9 100644 --- a/src/api/iot/product/product/index.ts +++ b/src/api/iot/product/product/index.ts @@ -16,6 +16,8 @@ export interface ProductVO { locationType: number // 设备类型 netType: number // 联网方式 codecType: string // 数据格式(编解码器类型) + protocolType?: string // 协议类型(STANDARD, MODBUS_RTU, MODBUS_TCP) + modbusSlaveId?: number // Modbus从站ID(1-247) deviceCount: number // 设备数量 createTime: Date // 创建时间 } diff --git a/src/api/iot/rule/data/sink/index.ts b/src/api/iot/rule/data/sink/index.ts index 3e2755e0d..5a70abc29 100644 --- a/src/api/iot/rule/data/sink/index.ts +++ b/src/api/iot/rule/data/sink/index.ts @@ -11,6 +11,7 @@ export interface DataSinkVO { config?: | HttpConfig | MqttConfig + | DatabaseConfig | RocketMQConfig | KafkaMQConfig | RabbitMQConfig @@ -79,6 +80,17 @@ export interface RedisStreamMQConfig extends Config { topic: string } +/** Database 配置 */ +export interface DatabaseConfig extends Config { + url: string // 数据库连接 URL + username: string // 数据库用户名 + password: string // 数据库密码 + driverClassName: string // 数据库驱动 + tableName: string // 目标表名 + fieldMapping: Record // 字段映射 + saveMetadata: boolean // 是否保存元数据 +} + /** 数据流转目的类型 */ export const IotDataSinkTypeEnum = { HTTP: 1, @@ -122,5 +134,31 @@ export const DataSinkApi = { // 查询数据流转目的(精简)列表 getDataSinkSimpleList() { return request.get({ url: '/iot/data-sink/simple-list' }) + }, + + // 自动创建数据库表 + autoCreateTable: async (data: { + url: string + username: string + password: string + driverClassName: string + tableName: string + fieldMapping: Record + saveMetadata: boolean + }) => { + return await request.post({ url: `/iot/data-sink/database/auto-create-table`, data }) + }, + + // 同步数据库表结构 + syncTableStructure: async (data: { + url: string + username: string + password: string + driverClassName: string + tableName: string + fieldMapping: Record + saveMetadata: boolean + }) => { + return await request.post({ url: `/iot/data-sink/database/sync-table-structure`, data }) } } diff --git a/src/api/iot/thingmodel/index.ts b/src/api/iot/thingmodel/index.ts index bcf9e0707..3b93c38c1 100644 --- a/src/api/iot/thingmodel/index.ts +++ b/src/api/iot/thingmodel/index.ts @@ -78,6 +78,18 @@ export interface ThingModelProperty { description?: string dataSpecs?: ThingModelProperty dataSpecsList?: ThingModelProperty[] + modbusConfig?: ModbusConfig +} + +/** Modbus 配置 */ +export interface ModbusConfig { + collectionMode?: number // 数据采集模式:1-定时轮询,2-主动上报 + functionCode?: number // 功能码:0x03-读保持寄存器,0x04-读输入寄存器等(默认0x03) + registerAddress?: number // 寄存器地址(低位字节) + registerCount?: number // 寄存器数量 / 线圈数量(默认1) + pollingInterval?: number // 采集频率(秒) + fullFrame?: string // 完整的数据帧(自动生成的HEX字符串) + crcHex?: string // CRC 校验码(自动生成的HEX字符串) } /** 物模型事件 */ @@ -101,6 +113,7 @@ export interface ThingModelService { inputParams?: ThingModelParam[] outputParams?: ThingModelParam[] method?: string + modbusConfig?: ModbusConfig // 添加 Modbus 配置 } /** 物模型参数 */ @@ -197,6 +210,11 @@ export const ThingModelApi = { // 删除产品物模型 deleteThingModel: async (id: number) => { return await request.delete({ url: `/iot/thing-model/delete?id=` + id }) + }, + + // 计算 Modbus 命令(用于前端自动生成) + calculateModbusCommand: async (data: { productId: number; registerAddress: number; functionCode?: number }) => { + return await request.post({ url: `/iot/thing-model/calculate-modbus-command`, data }) } } diff --git a/src/views/iot/product/product/ProductForm.vue b/src/views/iot/product/product/ProductForm.vue index 5247e9258..ec3c2cb16 100644 --- a/src/views/iot/product/product/ProductForm.vue +++ b/src/views/iot/product/product/ProductForm.vue @@ -74,7 +74,7 @@ - + +
Modbus 协议自动使用 HEX 格式
+
+ + + + + + + + + +
默认值:1
@@ -132,7 +153,9 @@ const formData = ref({ deviceType: undefined, locationType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + protocolType: 'STANDARD', + modbusSlaveId: 1 }) const formRules = reactive({ productKey: [{ required: true, message: 'ProductKey 不能为空', trigger: 'blur' }], @@ -152,6 +175,26 @@ const formRules = reactive({ const formRef = ref() const categoryList = ref([]) // 产品分类列表 +// 计算属性:是否为 Modbus 协议 +const isModbusProtocol = computed(() => { + return formData.value.protocolType === 'MODBUS_RTU' || formData.value.protocolType === 'MODBUS_TCP' +}) + +// 处理协议类型变化 +const handleProtocolTypeChange = (value: string) => { + if (value === 'MODBUS_RTU' || value === 'MODBUS_TCP') { + // Modbus 协议自动设置数据格式为 HEX + formData.value.codecType = 'HEX' + // 设置默认从站ID + if (!formData.value.modbusSlaveId) { + formData.value.modbusSlaveId = 1 + } + } else { + // 其他协议使用 Alink 格式 + formData.value.codecType = CodecTypeEnum.ALINK + } +} + /** 打开弹窗 */ const open = async (type: string, id?: number) => { dialogVisible.value = true @@ -208,7 +251,9 @@ const resetForm = () => { deviceType: undefined, locationType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + protocolType: 'STANDARD', + modbusSlaveId: 1 } formRef.value?.resetFields() } @@ -218,3 +263,11 @@ const generateProductKey = () => { formData.value.productKey = generateRandomStr(16) } + + diff --git a/src/views/iot/rule/data/sink/DataSinkForm.vue b/src/views/iot/rule/data/sink/DataSinkForm.vue index a4974575e..1f0f17e53 100644 --- a/src/views/iot/rule/data/sink/DataSinkForm.vue +++ b/src/views/iot/rule/data/sink/DataSinkForm.vue @@ -25,6 +25,10 @@ + + + + + + + + + + + + + + + + +
{{ tableNameError }}
+
如果配置了字段映射,可点击“自动建表”根据字段映射自动创建数据库表
+
+ +
+ + 同步表结构 + + 根据字段映射自动添加数据库表的列 +
+ +
可选配置。如果不配置,将使用设备消息中的字段名作为数据库列名
+
+ + +
+ 启用后会自动保存设备元数据:device_id(设备ID)、tenant_id(租户ID)、report_time(上报时间) +
+
+ + + + + diff --git a/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue index d0b115cdc..49a55e4d8 100644 --- a/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue +++ b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue @@ -1,7 +1,7 @@ diff --git a/src/views/iot/thingmodel/ThingModelService.vue b/src/views/iot/thingmodel/ThingModelService.vue index 35f7c9888..ffb9574b2 100644 --- a/src/views/iot/thingmodel/ThingModelService.vue +++ b/src/views/iot/thingmodel/ThingModelService.vue @@ -27,6 +27,52 @@ :direction="IoTThingModelParamDirectionEnum.OUTPUT" /> + + + Modbus 配置(可选) + + +
+ 如果配置了完整帧,将直接使用该帧发送命令(推荐用于固定命令) +
+
+ + +
+ 常用:05=写单线圈、06=写单寄存器、15=写多线圈、16=写多寄存器 +
+
+ + + + + +
+ 功能码05:65280(0xFF00)=闭合,0(0x0000)=断开 +
+