Skip to content

业务逻辑开发手册

基本概念

服务

服务是业务逻辑的配置单元。

开发页面点击发布后服务开始运行,在服务列表页可以手动停止或重新开启运行中的服务。

服务的配置有基本配置、节点。

服务的基本配置(服务名、描述)在服务列表页修改。

服务的变量、节点在开发页面修改。

节点

节点是服务的功能单元。

在开发页面中,节点之间以相互串联的形式组成服务的运行流程。

新版本的逻辑开发支持多个触发节点.

节点输出

有些节点会将其计算、运行的结果输出,供其他节点使用。

实例

每当服务的输入节点所设定的时间发生时,称服务被触发。

在实例运行时,如果产生错误(例如引用了不存在的输出、数值计算类型不匹配),实例会直接中止运行。

连线

节点之间分为数据连线与流程连线,其中流程连线控制着服务的执行走向,数据连线控制着值的传递,两条线以实线虚线区分。

img

两个节点之间存在实线或者虚线连线时,称这两个节点之间存在依赖关系。节点之间的依赖关系不能出现循环。因此所有节点构成有向无环图,服务中的所有节点存在拓扑顺序。

服务触发时,首先执行触发节点。当一个节点执行完成后,先根据其数据线输出值,然后根据其数据线和流程线输出,将一个或多个节点标记为待执行,多个待执行的节点之间按按照拓扑顺序执行。

标记待执行节点时,如果一个节点不接受流程线输入,则称它是一个数据节点,数据线输出到数据节点时,即会将数据节点标记为待执行;数据线输出到非数据节点时,不会将节点标记为待执行,只有流程线输出到非数据节点时,才会将节点标记为待执行。

img

图例A、B、C三个节点中,节点A是触发节点。节点A执行完成后,将节点B和节点C标记为待执行,此时节点B和节点C之间存在拓扑顺序,C依赖于B,因此节点B先执行。节点B执行完成后,将节点C标记为待执行。最后,节点C执行。

注意这里,节点B标记节点C为待执行时,因为节点C已经被节点A标记为待执行,因此不会重复标记,节点C只会执行一次。换言之,节点连线表达的是节点之间的依赖关系,而不是实际的执行路线,不能理解为A→C的连线与A→B→C的连线存在所谓的短路。

img

如图例A、B、C、D四个节点中,节点A是触发节点。节点A执行完成后,首先向节点C输出值,节点C是数据节点,因此被标记为待执行,然后节点C通过流程线将节点C标记为待执行。此时待执行节点B和C之间没有拓扑顺序,将以不确定顺序执行,我们假设节点B先于节点C执行。节点B执行后,首先向节点D输出数据,节点D是非数据节点,此时不会标记为带执行,然后节点B根据流程线将节点D标记为待执行。此时待执行节点为C和D,存在拓扑顺序,D依赖于C,因此C先执行。C执行完成后,输出数据给D,由于节点D是非数据节点,它不会被节点C标记为待执行,但它已经被节点B标记为待执行。最后,节点D执行。整体执行顺序是A→B→C→D(也可能是A→C→B→D)。

如果一个节点执行失败,它的流程线不会产生输出,它的所有数据线输出都是错误值。如果后续其他节点使用了错误值的输出,都会导致执行失败。

类型系统

逻辑开发系统使用的类型系统是JSON的超集,节点间传递和使用的值都遵从此类型系统。

值有以下类型:

  • boolean:与JSON相同
  • number:与JSON相同
  • string:与JSON相同
  • array:与JSON相同
  • map:与JSON的object相同
  • time:时间类型,不能从JSON中转换得到,转换为JSON时为字符串
  • binary:二进制类型,不能从JSON中转换得到,转换为JSON时为base64编码的字符串

此外,所有类型都可以是null值,对应JSON的null值。null值没有单独的类型,null值可以是任意类型,表示值不存在。节点的自定义输入可以要求值不能为空,一些节点(例如数学计算)的预定义输入值也会要求值不能为空,当通过连线输入的值为null时就会报错并中止节点运行。

模板字符串

一些节点的配置(例如HTTP响应的响应体、三方API请求的请求头、请求体)字段为模板字符串。

在模板字符串中,对于大括号${}括起的 path,按此 path 访问变量得到的值将以字符串形式替换到模板中。大括号和 path 之间可以有空格。 如果模板中包含美元符号本身,需要写两次以转义$$。 如果美元符号的下一个符号不是${,也表示美元符号本身。 在渲染到字符串时,字符串类型的值不会带有引号,如要保留引号,可以在 path 后加 | raw。 例如:输入变量x"false"payload{"args": ["ok"]},模板:

plain
arg0 = ${ payload.args[0] }
quoted arg0 = ${ payload.args[0] | raw }
x = ${x}
quoted x = ${ x | raw }
dolor: $
explicit escaped dolor: $$

渲染到字符串时将得到:

plain
arg0 = ok
quoted arg0 = "ok"
x = false
quoted x = "false"
dolor: $
explicit escaped dolor: $

模板JSON

模板JSON是模板字符串的一种特殊形式。一些节点的字段(例如数据库)要求是合法的JSON,但仍然可以在其中引用变量。

JSON中object的键被视为模板字符串进行替换。

JSON中object的值,如果是字符串,则被视为模板字符串进行替换。且当此模板字符串中只有一个${}而没有其他内容时,值将按对应类型替换到JSON。

例如参数payload{ x: 3 }

  • { "x": "${payload.x}" }替换后为{ "x": 3 },object 值的部分被替换,且替换为数值。
  • { "${payload.x}": "${payload.x}" }变量替换后为{ "3": 3 },object 键和值的部分被替换,但只有值的部分被替换为数值,键的部分仍为字符串。
  • { "x": "value = ${payload.x}" }变量替换后为{ "x": "value = 3" },object 值的部分被替换,但由于path的两侧还有其他字符,替换后仍为字符串形式。
  • { "x": "${payload.x} + ${payload.x}" }变量替换后为{ "x": "3 + 3" },object 值的部分被替换,但由于存在多个path,因此替换后仍为字符串形式。

数据定义

节点的数据定义分为预定义数据与自定义数据,有些节点如HTTP节点的数据输入与输出是未知的,这些需要自己在输入值/输出值中配置。

img

输入值

属性名说明
名称属性的名称,如名称为 age, 后续在节点中引用此属性为 ${ age }
必填如果此项为开,当节点执行时,此输入值为空则会报错停止执行,否则会该节点仍会尝试是否可以继续执行

输出值

属性名说明
名称属性的名称,如果当路径过长影响观感时,可以使用名称属性来说明,为空时节点上会显示路径值
引用路径引用路径决定了该输出值的内容当该节点抛出了一个对象。如: { name: "A", age: 9 }路径填写为 name,只会抛出 name 属性如果路径为空,则默认抛出整个对象路径为空的输出值仅允许存在一个

输出路径

一些节点(如数学计算)只输出一个值,而另外一些节点(如测点订阅)可能会输出多个值。输出多个值时,以map类型进行输出,如果节点的输出是自定义的,可以指定path从输出值中摘取部分内容作为选择。path为空则输出完整的输出值。

path由以下三种形式自由组合而成

  1. 下标访问:当要访问的值是array时,可用方括号括起的十进制整数访问,例如[0][1]。 方括号和数字之间可以有空格。
  2. 名称访问:当要访问的值是map时,可以用点号加标识符访问,例如.a.b。 标识符的规则是字母或下划线开头,后接零或多个字母、数字、下划线。 当整个路径是以名称访问开头时,第一个点号可以省略。
  3. 带引号的名称访问:名称访问的另一种形式,如果名称不符合标识符的规则,可以用方括号括起一个单引号或双引号括起的属性名,在引号中,可以用反斜线转义特殊字符。 例如["a"]['b'] 方括号和引号之间可以有空格。

反斜线能转义的特殊字符如下:

  • \n
  • \r
  • \t
  • \\:反斜线本身
  • \':单引号本身,在双引号字符串中单引号可以不转义
  • \":双引号本身,在单引号字符串中双引号可以不转义
  • \u{0000}:一个Unicode字符,其中十六进制数0000替换成Unicode值

如果下标访问的不是array,或者名称访问的不是 map,访问会出错。 如果下方访问时,下标溢出array长度,访问会出错。

例如,如果节点输出是{"a": {"b": ["ok"]}},则路径a输出的是{"b": ["ok"]},路径a.b输出的是["ok"]。以下几种path都能输出"ok"

  • a.b[0]
  • .a.b[0]
  • ["a"].b[0]
  • ["a"]['b'][0]

如何发布服务

img

框中的两个按钮,左侧的为保存按钮,右侧的为发布按钮。

**保存:**当服务没有完全配置完成时,点击保存可以将目前的进度保存起来。这时服务的运行并不会更新。

**发布:**发布节点决定了当服务运行时的配置,在发布前会对所有节点的配置进行校验, 如果校验失败则会提示出错的位置,当全部节点校验成功后服务才会进入发布状态。

服务发布历史

当发布的服务在运行阶段出现了错误或对当前的调整不满意时,可以通过恢复发布版本的方法来进行回滚

img

img

点击发布历史或右下角的按钮来显示选中服务的发布历史

如何调试

当服务运行起来后,想要查看服务的实时运行状态可以点击服务日志来查看。

img

img

功能介绍

按照不同节点的功能,我们将节点分成了 5 个分组

触发节点

触发节点是服务流程的发起节点, 当满足触发条件时(如时间/HTTP请求等), 服务才会被触发

定时触发

在规定的时间内执行的节点。

定时触发节点用于设置时间,使服务在指定时间执行。常用于定时推送消息、定时执行任务、定时提醒、定时触发设备等场景。

节点配置

在业务逻辑开发编辑页面的 触发节点 中,拖拽 定时触发 到中间画布并进行配置

配置项说明
节点名称设置节点名称。支持中文汉字、英文字母、数字和下划线(_),长度不超过20个字符。
触发配置当触发规则满足时,即可触发流程
触发模式单次触发 (只触发一次):选择具体触发时间, 精确到秒。间隔固定分钟数触发:按分钟间隔进行规则出发,需要设置具体的时间间隔分钟数,范围 1~59分钟。间隔固定小时数触发:按小时时间间隔进行规则触发。需设定具体的时间间隔小时数,范围:1-23小时。每天固定时间触发一次:每天的固定时间进行触发。需设定具体的触发时间点,可精确到秒。**每周固定时间触发:**每周的固定时间进行触发。需选择周内触发日期(工作日、周末或周内某天)和具体的触发时间点,可精确到秒。每月固定时间触发:每月的固定时间进行触发。需选择月内具体日期(或选择最后一天)和具体触发时间点,可精确到秒。选择生效时间。选择结束时间。不设置该内容为永久生效
执行时间需要流程触发的时间单次触发模式时,该选项为具体的日期时间( yyyy-mm-dd hh:mm:ss )以天为间隔触发时,该选项为具体的时间( hh:mm:ss )以周为间隔触发时,该选项为具体的星期时间 (星期几 hh:mm:ss)以月为间隔触发时,该选项为具体的号数时间 (几号 hh:mm:ss)
循环间隔循环的间隔时间以分钟为间隔触发时,该选项为分钟数值 (1~59)以小时为间隔触发时,该选项为小时数值 (1~24)
生效时间必填:当模式不为单次触发时显示,循环开启的时间 ( yyyy-mm-dd hh:mm:ss )
结束时间可选:当模式不为单次触发时显示,循环结束的时间 ( yyyy-mm-dd hh:mm:ss )
节点输出
输出属性说明
输出时间当前节点触发的时间
触发模式的一些细节
  1. 间隔固定分钟数、间隔固定小时数触发时,生效时间不仅用于限定触发时间,还用于决定触发时间具体的时、分、秒。例如生效时间是某天0点,间隔2小时触发,则实际的触发时间是0点、2点、4点……对于其他触发模式,则需要手动指定具体的时、分、秒,生效时间只用于限定触发时间。
  2. 每月固定时间触发时,如果选择29、30、31日,但某月没有29、30、31日,则当月不触发
触发丢失

如果服务器停机维护,本应在停机期间定时触发的服务,在服务器重启后不会补齐

触发延迟

受服务器负载影响,实际触发时间可能会比预定时间略有延迟,延迟期间额外的触发计划不会补齐。例如预定某日00:00:00触发,实际触发时间可能是00:00:05,即使00:00:01还有一次预定触发,则00:00:05也触发一次。

测点订阅

每当设备点位上报信息时,触发服务的运行。

设备触发节点是将设备上报的属性(读写型)、事件数据或状态变更作为服务的输入,触发服务后续的业务逻辑。设备触发节点支持通过虚拟设备上报属性或事件触发服务,帮助您自定义设备信息响应的服务流。

节点配置

在业务逻辑开发编辑页面的 触发节点 中,拖拽 测点订阅 到中间画布并进行配置

配置项说明
节点名称设置节点名称。支持中文汉字、英文字母、数字和下划线(_),长度不超过20个字符。
点位选择选择需要监测的设备点位
节点输出
输出属性说明
设备 ID选中设备的 ID
点位 ID选中点位的 ID
输出值当前点位触发时上传的点位值
输出时间当前节点触发的时间

HTTP 触发

通过 HTTP 请求 ([POST] topstack的服务地址/logic/api/v1/dataflow/你的服务ID/trigger) 触发节点

HTTP触发节点是创建API服务的开始节点,通过该节点可配置API的请求参数和SDK调用时的Action。每个 API 仅有一个HTTP触发节点可以生效,中间逻辑节点可根据业务需要选择其他功能节点.

节点配置

在业务逻辑开发编辑页面的 触发节点 中,拖拽 HTTP触发 节点到中间画布并进行配置

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
认证方式是否需要做账号鉴权的选项:不使用鉴权**bearer:使用 token 进行鉴权basic:**使用用户名与密码的方式进行鉴权
token当认证方式为 bearer 时必须填写
用户名当认证方式为 basic 时必须填写
密码当认证方式为 basic 时必须填写
节点输出

节点的输出即是触发请求的请求体。其类型依赖于触发请求的Content-Type请求头。

如果Content-Typetext/plain,则请求体整体被解析为字符串,再作为节点输出。

如果Content-Typeapplication/json,则请求体整体被解析为JSON,再作为节点输出。

如果请求头不含Content-Type,请求体被忽略,节点输出为null。

除此之外的Content-Type都会导致HTTP响应406 Not Acceptable。

输出属性说明
unknown该节点为动态输出属性
请求认证

节点可以设置Basic或Bearer认证:

  • Basic:触发请求应有请求头Authorization: Basic {token},其中{token}应为节点指定的用户名、密码,中间以冒号拼接后,再用base64编码的字符串。例如用户名为hello,密码为world,则{token}应替换为hello:world的base64编码,即aGVsbG86d29ybGQ=
  • Bearer:触发请求应有请求头Authorization: Bearer {token},其中{token}应为节点指定的token

未设置认证时,请求不需要Authorization请求头,如有则被忽略。

认证未通过会导致HTTP响应401 Unauthorized。

触发请求

向平台发送如下HTTP请求即可触发:

POST /logic/api/v1/dataflow/{service_id}/trigger

其中{service_id}替换为服务的ID。

触发成功时,触发请求的响应码一般由HTTP响应节点指定。以下响应码有可能不是由HTTP返回节点指定的:

响应码原因
201 Created①实例已完成,但没有终止于HTTP返回节点 ②实例在一定的时间内没有终止 ③实例由于运行节点数超过限制而被中止
400 Bad Request无法按照指定的Content-Type解析请求体
401 Unauthorized节点开启了认证,但Authorization不存在或与没有通过认证
404 Not Found指定的服务不存在,或服务没有HTTP触发节点
406 Not AcceptableContent-Type不是text/plain或application/json
413 Request Entity Too Large请求体过大
500 Internal Server Error服务器内部错误
502 Bad Gateway服务器内部错误
503 Service Unavailable服务器内部错误
504 Gateway Timeout服务器内部错误

MQTT订阅

订阅 MQTT 消息触发节点执行。

NOTE

接收到的 MQTT 消息内容只支持 JSON 格式

节点配置

在业务逻辑开发编辑页面的 触发节点 中,拖拽 MQTT 订阅 节点到中间画布并进行配置

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
MQTT 版本协议版本。
心跳单位秒。
地址接入地址和端口。
主题订阅主题,支持通配符。
用户名&密码
节点输出

如果要从消息体中解析值作为节点输出,消息体必须是 JSON 格式。

输出点由用户自定义实现,每个输出点包括【名称】和【引用路径】两个字段。

  • 名称:输出点的名称
  • 引用路径:用于计算输出点对应的输出值,格式为 json 对象的字段路径。

示例1:json 内容为 { "obj": { "value": 100 } }, 如果要获取 value 的值,则路径为:obj.value

示例2:json 内容为 [{ "obj": { "value": 100 } }], 如果要获取 value 的值,则路径为:[0].obj.value

NOTE

引用路径可以为空,如果为则输出整个 json。

告警输出

设备产生告警时,触发该节点执行。

节点配置

在业务逻辑开发编辑页面的 触发节点 中,拖拽 告警出发 节点到中间画布并进行配置

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
设备选择需要监测告警的设备
节点输出
输出属性说明
设备 ID输出告警的设备的ID
告警标题-
告警内容-
输出时间-

控制节点

条件判断

条件判断节点根据设定的条件对输入值进行判断,再根据判断结果执行不同的路径。

节点配置

在业务逻辑编辑页面的节点列表中,选择 控制节点 下的 条件判断 节点。

配置项说明
节点名称设置节点名称。支持中文汉字、英文字母、数字和下划线(_),长度不超过30个字符。
判断条件单击 添加条件 添加判断条件,您可以为当前节点添加多个条件。
仅变化触发当该选项为 true 时, 则记录比较结果,如果比较结果与上一次相同,截断流程。

image-20250704134309807

判断条件说明:

  • 用于比较的数据源,可设置为:

    • 常量:静态数据。可选数据类型:

      • 数值型:输入数字,可以包含小数点。

      • 布尔值:设置为true或者false。

      • 字符串:输入字符串数据。

      • 时间型:可选择具体时间点。

      • JSON:可以用名称或值对的方式来表达复杂的数据格式,需要采用JSON格式书写。对象可以包含多个名称或值对。例如{ "firstName":"John" , "lastName":"Doe" }

    • 来自其他节点

  • 比较方式:大于、大于等于、小于、小于等于、等于、不等于。

当有多个条件时,根据条件的前后顺序,从上至下依次进行逻辑运算:

  • AND:表示逻辑与运算。
  • OR:表示逻辑或运算。

例如:条件 1 为 true,条件 2 为 false,条件 3 为 true,依次设置条件关系为 ANDOR,则对应的表达式为: true && false || true,运算结果为 true。

节点输出

该节点无数据输出

延迟器

延迟器对输入流程,开启一个定时器,定时结束后输出流程。

定时器结束前的输入会重置定时器,Cancel上的输入可以取消最后一个定时器。

节点配置

在业务逻辑编辑页面的节点列表中,选择 控制节点 下的 延迟器 节点,节点配置页面如下图所示。

img

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
延迟时间定时器间隔
节点输出

该节点无数据输出

限流器

限流器对输入流程,开启一个定时器,在规定时间内此节点仅执行指定次数。

根据设置间隔时间窗口,控制每个时间窗口内的执行次数不超过limit次。

输入流程时,如果时间窗口不存在,则将本次输入记为第 1 次,输出流程,并开启时间窗口。

如果在时间窗口内,输入次数N大于limit,则从本次输入流程被截断(不输出)。

节点配置

在业务逻辑编辑页面的节点列表中,选择 控制节点 下的 限流器 节点.

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
间隔时间定时器间隔
次数限制在间隔时间内允许执行的次数
节点输出

该节点无数据输出

计算节点

数值计算

对输入项进行计算求值, 仅支持对所有数据进行相同计算

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 数值计算 节点.

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
计算方式求和求差求积求商取最大值取最小值平均值中间值差分运算标准偏差
节点输出
输出属性说明
输出值计算后的结果

数学运算

数学运算数值计算基本相同, 区别只在于数学运算提供了数值计算所不具备的计算方法, 但是只能计算一个数值。

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 数学计算 节点.

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
计算方式取绝对值、取反、取倒数四舍五入、向下取整、向上取整、向零取整正弦计算、余弦计算、正切计算、余切计算、反正弦计算、反余弦结算、反正切计算、余切计算双曲正弦计算、双曲余弦计算、双曲正切计算、反双曲正弦计算、反双曲余弦计算、反双曲正切计算开平方、开立方、对数计算、指数计算
节点输出
输出属性说明
输出值计算后的结果

组合运算

组合运算可以自由组合计算的方式来对数值进行操作,当需要对一个值通过复杂的计算和条件判断来获取结果时,使用该节点可以让你的逻辑更清晰,简洁。

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 组合运算 节点。

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
节点输出

该节点为动态输出

比较运算

比较运算节点与条件判断节点功能相似,区别在于条件判断节点没有数据输出,比较节点会输出一个布尔类型的比较结果,比较运算也没有流程输出。

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 组合运算 节点。

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
判断条件==、!=>、>=<、<=区间判断: 判断接入值是否位于设定的区间内
区间设置 (判断条件 = 区间判断)全开区间、全闭区间、左开右闭、左闭右开区间值
节点输出

该节点输出比较后的结果,类型为布尔值

逻辑运算

逻辑运算提供了对两个值进行逻辑判断的功能

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 逻辑运算 节点。

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
判断条件逻辑与逻辑或逻辑异或逻辑非
节点输出

该节点输出比较后的结果,类型为布尔值

聚合运算

聚合运算节点在服务启动后设置一个定时器,每间隔固定时间窗口,对窗口内所有接收到的数据的进行聚合统计,并输出一个值。

节点配置

在业务逻辑编辑页面的节点列表中,选择 计算节点 下的 逻辑运算 节点。

配置项说明
节点名称支持中文汉字、英文字母、数字和下划线(_)。长度不超过20个字符。
时间窗口聚合统计的时间长度
聚合方式最大值、最小值、中间值(数据个数为偶数则为中间两个数的平均值)、平均值、首值、末值、差分(最大值减最小值)、求差(末值减首值)、标准偏差

如果输入数据中存在空值或非法(不是数值类型)值,这个值不会参与计算。尤其注意求平均值时,空值或非法值不会计入分母,如果需要参与计算应使用其他节点将值强制转换为0。

节点输出

该节点输出聚合计算后的结果,类型为数值型

积分运算

积分运算在服务启动后设置一个定时器,每间隔一个固定的时间窗口,将窗口内的输入值视为某种速度,求得积分。

例如,输入值是监测到的瞬时流速,时间窗口为 5分钟,则服务启动后每5分钟输出一次这5分钟内的累计流量。

积分运算默认将输入值的时间粒度视为秒,如果输入值的时间粒度不是秒,应当设置时间颗粒度。例如流速 60 m³/min,积分运算默认将输入值 60 视为 60 m³/s,假如时间窗口是 5秒,则 5秒 后输出 300,即 300 m³;而实际上,流速应当是 1 m³/s,5秒时间窗口内的累计流量应是 5 m³,此时就应当将时间颗粒度设置为 60秒,使积分结果除以 60。

img

如果节点没有获得输入值,则时间窗口结束时输出null值。时间窗口内的末值到时间窗口结束这一段时间视为矩形区域进行计算(上图中蓝色区域),并将此末值视为下一个时间窗口的首值。

导数运算

导数运算在服务启动后第二个输入值开始,每输入一个值,对应将前后两个值之间的差值除以两个值之间的时间差,求得瞬时导数,作为某种速度。

例如,输入值是监测到的累计流量,则每当累计流量发生变化时,输出此次变化相对前值的流速。

导数运算默认将时间间隔换算为秒,如果想要输出不是时间粒度不是秒,应当设置时间颗粒度。例如流量 10 m³、20 m³,两条数据间隔 1s,输出值 10,表示 10 m³/s,将时间颗粒度设置为 60秒,则使导数结果乘以 60,输出 600,表示 600 m³/min。

功能节点

常量节点

创建一个常量来供其他节点使用

节点配置

在业务逻辑编辑页面的节点列表中,选择 功能节点 下的 常量节点 节点。

节点输出

常量节点的数据输出为它本身

变量查询 & 变量设置

对系统的变量进行操作

变量节点操作的变量配置地址位于 /ui/basic/global-variables

节点配置

在业务逻辑编辑页面的节点列表中,选择 功能节点 下的 变量查询/变量设置 节点。

节点输出

仅当变量查询时输出选择的变量

设备查询 & 指令下发 & 属性下发

设备查询:对设备点位/属性进行查询

指令下发:对设备点位进行数据下发

属性下发:对设备属性进行更改

节点配置

在业务逻辑编辑页面的节点列表中,选择 功能节点 下的 设备查询**/**指令下发/属性下发 节点。

选择要操作的 设备 -> 点位/属性

节点输出

设备查询:输出查询的点位/属性

Javascript

使用 Javascript 节点来实现一些对于其他节点来说可能无法实现或实现复杂的操作。

节点配置

在业务逻辑编辑页面的节点列表中,选择 功能节点 下的 Javascript 节点。

在代码编辑器中输入的所有代码都会被自动包裹在一个 function main() {}

代码案例

javascript
// 下面的方法实现在 JS 节点中接入两个数值, 并将两个数值的和输出

/**
* 在输入值中配置 num1, num2 两个输入值
* input: { num1: number; num2: number }
*/
const { num1, num2 } = input
return num1 + num2  // 如果不使用对象包裹,在输出值配置中,应将 path 设置为空
return { sum: num1 + num2 } // 如果使用对象包裹,在输出值配置中,path 应该为 sum

img

注意:当输出值 path 为空时,name 应为必填

类型关系
JavaScript类型(typeof)输出类型
undefinednull
symbolstring
stringstring
numbernumber
bigintnumber
functionstring,值为"[Function $name]"
booleanboolean
object如果值为null,输出null如果是特定实例,按对应类型输出否则,输出map,键为其getOwnPropertyNames

特定实例如下

object实例类型(instanceof)输出类型
Numbernumber
Booleanboolean
Stringstring
Datetime
Arrayarray
Mapmap
Setmap,值皆为null
Errorstring,值为toString()返回的字符串
ArrayBufferbinary
全局变量 input
typescript
const input: { [variable: string]: unknown }

全局变量input为一个Proxy,用于获取输入参数的值。

javascript
console.log(input.V1); // 查询参数 V1 的值并打印到日志
全局变量 env
typescript
const env: { [namespace: string]: NamespaceProxy }
type NamespaceProxy = { [variable: string]: unknown }

全局变量env为一个Proxy,以命名空间为键可以获得命名空间的Proxy。

命名空间的Proxy以变量名(包括局部变量和全局变量)为键可以查询变量的值,变量的值可以在代码中修改。

javascript
console.log(env.ns1.variable1); // 查询命名空间 ns1 下的变量 variable1 的值并打印到日志
env.ns1.variable1 = 1; // 将命名空间 ns1 下的变量 variable1 的值设为 1
全局变量 console
typescript
const console: {
  log: (...args: any[]) => void;
  debug: (...args: any[]) => void;
  info: (...args: any[]) => void;
  warn: (...args: any[]) => void;
  error: (...args: any[]) => void;
}

全局变量console为一个object,其方法debuginfo(等同于log)、warnerror可以用于向日志打印消息。上述方法会将每个参数转为字符串(某种程度上展示其内容,与toString()不相同),以空格为间隔拼接。

注意:这个对象与Web APInode.js不兼容

全局变量 device
typescript
const device: { [deviceID: string]: Device }

type Device = {
  point: DevicePointAPI;
  prop: DevicePropertyAPI;
}

interface DevicePointAPI {
  get(this: DevicePointAPI, pointID: string): unknown;
  get(this: DevicePointAPI, ...pointIDs: string[]): { [pointID: string]: unknown };
  set(this: DevicePointAPI, pointID: string, value: any): void;
  set(this: DevicePointAPI, Map<string, any> | { [pointID: string]: any }): void;
}

interface DevicePropertyAPI {
  get(this: DevicePropertyAPI, propertyID: string): unknown;
  get(this: DevicePropertyAPI, ...propertyIDs: string[]): { [propertyID: string]: unknown };
}

全局变量device为一个Proxy,以设备ID为键可以获取到对应设备的Device对象。获取Device对象时,不会检查设备是否存在。

Device对象为绑定到某台具体设备的一个object。其属性point为同一设备的DevicePointAPI对象,prop为同一设备的DevicePropertyAPI对象。

DevicePointAPI对象为绑定到某台具体设备的一个object,用于访问其点位。方法get传入点位ID用于查询一个点位值(点位没有值时返回null),传入多个点位ID时返回值为object形式。方法set传入点位ID和点位值时用于下发点位值,传入一个object时将以其键为点位ID、值为点位值一次性下发多个点位值,也可以传入一个Map

DevicePropertyAPI对象和DevicePointAPI对象类似,用于访问设备的属性。目前只能查询而不能修改。

javascript
// 查询设备 SD1 点位 V1 的值
console.log(device.SD1.point.get('V1') || 0.0); // => 42.0
// 查询设备 SD1 点位 V1、V2 的值
console.log(device.SD1.point.get('V1', 'V2')); // => { "V1": 42.0, "V2": 11.5 }
// 下发设备 SD1 点位 V1 的值
device.SD1.point.set('V1', 60);
// 下发设备 SD1 点位 V1、V2 的值
device.SD1.point.set({ V1: 60, V2: 45 });
// 查询设备 SD1 属性 label 的值
console.log(device.SD1.prop.get('label')); // => "golden"
// 查询设备 SD1 属性 label、state 的值
console.log(device.SD1.prop.get('label', 'state')); // => { "label": "golden", "state": "ready" }
节点输出

JS节点的数据输出为自定义输出

Starlark

Starlark 节点与 Javascript 节点功能相同,不过需要使用 Starlark 语言,这种语言类似于 python 语言。

Starlark 语言的详细内容可以参考 :https://docs.bazel.build/versions/4.2.1/skylark/language.html

在代码编辑器中输入的所有代码都会被自动包裹在一个 def main(): 的缩进层级中,如果代码为空,则自动插入一个pass

代码案例

python
# 下面的方法实现在 Starlark 节点中接入两个数值, 并将两个数值的和输出
# 在 Starlark 节点中,使用 input.get(属性名) 来获取属性

return input.get("num1") + input.get("num2") # 默认返回
return {
    "sum": input.get("num1") + input.get("num2") # 以 map 形式返回
}
类型关系
输入类型Starlark类型(type)
nullNoneType
stringstring
number如果包含小数部分则为float,否则为int
mapdict
arraylist
timeint,值为对应的毫秒级UNIX时间戳
Starlark类型(type)输出类型
NoneTypenull
stringstring
int、floatnumber
functionstring,值为"[function $name]"
builtin_function_or_methodstring,值为"[function $name]"
dictmap,仅包含string类型键的值
setmap,值皆为null
list、tuplearray
modulestring,值为"[module $name]"
内置模块 input
  • input.get(variable: str) -> Any获取节点输入
python
log.debug(input.get("V1")) # 获取输入参数 V1 的值并打印到日志
内置模块 env
  • env.get(namespace: str, variable: str) -> Any查询变量(包括局部变量和全局变量)
  • env.set(namespace: str, variable: str, value: Any)修改变量
python
log.debug(env.get("ns1", "variable1")) # 查询命名空间 ns1 下的变量 variable1 的值并打印到日志
env.set("ns1", "variable1", 1) # 将命名空间 ns1 下的变量 variable1 的值设为 1
内置模块 log
  • log.debug(*args: Any)
  • log.info(*args: Any)
  • log.warn(*args: Any)
  • log.error(*args: Any)

打印日志。

内置模块 device
  • device.get(device_id: str, point_id: str) -> Any查询单个点位值。参数为设备标识、点位标识。点位没有值时返回None
  • device.get(device_id: str, point_id: str, *more_point_ids: str) -> dict[str, Any]查询多个点位值,返回dict。
  • device.set(device_id: str, point_id: str, value: Any)下发单个点位值,参数为设备标识、点位标识、值。
  • device.set(device_id: str, point_values: dict[str, Any])下发多个点位值,参数为设备标识、点位标识和值的dict。
  • device.prop(device_id: str, prop_id: str) -> Any查询单个属性值。参数为设备标识、属性标识。属性没有值时返回None
  • device.prop(device_id: str, prop_id: str, *more_prop_ids: str) -> dict[str, Any]查询多个属性值,返回dict。
python
# 查询设备 SD1 点位 V1 的值
log.debug(device.get('SD1', 'V1') or 0.0) # => 42.0
# 查询设备 SD1 点位 V1、V2 的值
log.debug(device.get('SD1', 'V1', 'V2')) # => { "V1": 42.0, "V2": 11.5 }
# 下发设备 SD1 点位 V1 的值
device.set('SD1', 'V1', 60)
# 下发设备 SD1 点位 V1、V2 的值
device.set('SD1', { "V1": 60, "V2": 45 })
# 查询设备 SD1 属性 label 的值
log.debug(device.prop('SD1', 'label')) # => "golden"
# 查询设备 SD1 属性 label、state 的值
log.debug(device.prop('SD1', 'label', 'state')) # => { "label": "golden", "state": "ready" }
节点输出

Starlark 节点的数据输出为自定义输出

数据库操作

基础:SqlValue
typescript
type SqlValue =
  | number
  | boolean
  | null
  | Template

SqlValue表示参与运算的值。有以下几种形式:

  • number类型的常量
  • boolean类型的常量
  • null
  • 一个模板字符串

对于模板字符串,其根据模板本身的格式,有以下几种情况:

  • 字符串常量:如果模板字符串中不包含变量替换,则被视为字符串常量参与运算。如"hello"
  • 模板字符串:如果模板字符串中包含不止一个变量替换,或者除了变量替换之外还有其他字符(包括空格),则在进行变量替换后,以字符串形式参与运算。如"x is ${x}"
  • 模板值:如果模板字符串中有且仅有一个替换,且变量替换的两边没有其他字符(包括空格),则在进行变量替换后,按照替换结果的类型参与运算。如"${ payload.x }"

对于最后一种模板值的情况,如果替换结果为numberbooleannullstringtime类型的值,则使用此值参与运算;如果替换结果为arraymap类型的值,则会再次转换为string类型再参与运算。

基础:Where

Select、Update、Delete都需要使用此Where格式。

Where可以直接设为一个SqlValue

数据库节点中想引用接入的变量直接使用 ${变量名} 即可

如果这个SqlValuenull,则Where本身被忽略。否则,Where的值将会被数据库隐式转换为布尔值使用,包括模板值替换之后得到的null(被数据库识别为false)。

需要直接使用SqlValue作为Where场景比较少见。一种使用场景是,如果数据库开启了严格检查,不带WHEREUPDATEDELETE语句会报错:

json
{
  "table": "users",
  "where": true
}
DELETE FROM users WHERE true;
Where 的简单键值形式

Where的简单键值形式是一个JSON对象,其键为列名,如果值为SqlValue则表示相等:

json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "age": "${ age }"
  }
}
-- 输入参数:
--   age: 35
SELECT id FROM users WHERE age = 35;

多个键会按照字母表顺序排列后用AND链接。如果有逻辑短路的需求,需要用复杂形式严格指定顺序。

json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "is_manager": true,
    "age": "${ age }"
  }
}
-- 输入参数:
--   age: 35
SELECT id FROM users WHERE age = 35 AND is_manager = true;

简单键值形式的值可以是一个键为"$eq""$neq""$gt""$gte""$lt""$lte""$like"之一特殊值,值为SqlValue的JSON对象,分别表示等于、不等于、大于、大于等于、小于、小于等于、LIKE

json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "age": {"$gte": 60}
  }
}
SELECT id FROM users WHERE age >= 60;

这种值必须只有一对键值,否则不保证哪一种特殊键被识别。

json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "age": {"$gte": 60, "$lt": 80}
  }
}
SELECT id FROM users WHERE age >= 60;
-- 也有可能是:
-- SELECT id FROM users WHERE age < 80;

简单键值形式的列名不能以美元符号开头,如果有此需求,需要用复杂形式表示。

如果简单键值形式的键值数量为0,则WHERE本身被省略。

Where 的复杂形式

Where的复杂形式是一个JSON对象,其键为美元符号开头的特殊键。

复杂形式只能有一对键值,否则不保证哪一种特殊键被识别。

特殊键如下:

  • "$expr":值应为一个SqlValue,表示一个常量值
  • "$column":值应为一个字符串常量,表示列名
  • "$and""$or":值应为一个嵌套的Where的列表(包括SqlValue、简单键值形式、复杂形式),列表长度不限,表示ANDOR。如果列表长度为0,则不生成SQL表达式。
  • "$not":值应为一个嵌套的Where,表示NOT
  • "$eq""$neq""$gt""$gte""$lt""$lte""$like":值应为一个嵌套的Where的列表(包括SqlValue、简单键值形式、复杂形式),且列表长度必须为2,分别表示等于、不等于、大于、大于等于、小于、小于等于、LIKE
json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "$or": [
      {"is_manager": true},
      {"age": {"$gte": 60}},
      {"$neq": [{"$column": "locked"}, false]}
    ]
  }
}
SELECT id FROM users
WHERE
  is_manager = true OR
  age >= 60 OR
  users."locked" <> false;

这个例子中可以看到,WHERE为使用了特殊键"$or"的复杂形式,前两个嵌套的Where是简单形式,并且没有按字母表顺序重新排序,最后第三个嵌套的Where是复杂形式。

特别地,复杂形式如果是"$and"的话,可以简写为一个列表。

运算符优先级

使用复杂形式的时要注意,需要用嵌套关系来表达运算符优先级关系。

sql
SELECT id FROM users
WHERE
  is_manager = true OR
  age >= 60 AND
  locked = true;

在这个例子中,因为AND的优先级比OR高,所以使用括号显式表达的语句是这样的:

sql
SELECT id FROM users
WHERE
  is_manager = true OR
  (age >= 60 AND locked = true);

代码如下:

json
{
  "table": "users",
  "row": ["id"],
  "where": {
    "$or": [
      {"is_manager": true},
      [
        {"age": {"$gte": 60}},
        {"locked": true}
      ]
    ]
  }
}

如果需要先结合OR两侧的条件,应当显式进行结合:

sql
SELECT id FROM users
WHERE
  (is_manager = true OR age >= 60) AND
  locked = true;
{
  "table": "users",
  "row": ["id"],
  "where": [
    {
      "$or": [
        {"is_manager": true},
        {"age": {"$gte": 60}}
      ]
    },
    {"locked": true}
  ]
}

这个例子同时展示了将"$and"简写为列表。

Select
typescript
type Select = {
  'table': string;
  'row': '*' | SelectColumn[];
  'where'?: Where;
  'limit'?: number;
  'offset'?: number;
  'order'?: OrderBy[];
  'group'?: string[];
}

type SelectColumn =
  | Selectable
  | { [alias: string]: Selectable }

type Selectable =
  | string
  | { "$column": string }
  | { "$count": string | null }
  | { "$max": string }
  | { "$min": string }

type OrderBy =
  | string
  | { "$asc": string }
  | { "$desc": string }
{
  "table": "users",
  "row": [{"user_id": "id"}, "name"],
  "where": {
    "name": "admin",
    "locked": false
  }
}
SELECT id AS user_id, name FROM users WHERE name = 'admin' AND locked = false;

Select的输出为列表,列表元素为查询结果的行。

json
[
  {"user_id": "0001", "name": "john"},
  {"user_id": "0002", "name": "ruby"}
]
SELECT *

将row设为字符串"*"可以实现SELECT *

json
{
  "table": "users",
  "row": "*",
  "where": {
    "name": "admin",
    "locked": false
  }
}
SELECT * FROM users WHERE name = 'admin' AND locked = false;
简单聚合

Select支持几种简单聚合,为以下几种特殊键的JSON对象,值为列名:

  • "$count"
  • "$max"
  • "$min"

特别地,{ "$count": null }表示COUNT(*)

注意聚合不能直接作为row,而需要作为其元素。

json
{
  "table": "users",
  "row": [{ "$count": null }],
  "where": {
    "name": "admin",
    "locked": false
  }
}
SELECT COUNT(*) FROM users WHERE name = 'admin' AND locked = false;
聚合语义校验

使用聚合时,非聚合的列必须在group中使用。

json
{
  "table": "users",
  "row": [{ "$count": null }, "age"]
}

此例中使用了聚合,但非聚合列age没有在group中使用。

修正方法是将非聚合列age添加到group中。

json
{
  "table": "users",
  "row": [{ "$count": null }, "age"],
  "group": ["age"]
}

另一种修正方法是将非聚合列age改为聚合列。

json
{
  "table": "users",
  "row": [{ "$count": null }, { "$max": "age" }]
}

另外,如果group不为空,即使row"*"或者row中没有聚合列,当前操作也视为使用了聚合。

json
{
  "table": "users",
  "row": ["age"],
  "group": ["is_manager"]
}

聚合语义在发布时进行校验,并非在运行时由数据库进行校验。

排序

order为排序列的列表,可以用特殊键"$asc""$desc"的JSON对象显式表示升序或降序:

json
{
  "table": "users",
  "row": "*",
  "order": ["age", { "$desc": "name" }, { "$asc": "id" }]
}
SELECT * FROM users ORDER BY age, name DESC, id ASC;
Insert
typescript
type Insert = {
  'table': string;
  'rows': InsertRow[];
}

type InsertRow = { [column: string]: SqlValue }
{
  "table": "users",
  "rows": [
    {"name": "my name is ${ john }", "age": 35},
    {"name": "my name is ${ ruby }", "age": 40}
  ]
}
-- 输入参数
--   john: "John Ted"
--   ruby: "Ruby Gem"
INSERT INTO users (name, age) VALUES (
   ('my name is John Ted', 35),
   ('my name is Ruby Gem', 40)
);

生成的语句中,列名取所有输入行中列名的并集,如果某输入行缺少对应列,以NULL填充:

json
{
  "table": "users",
  "rows": [
    {"id": 1},
    {"id": 2, "name": "Wink"}
  ]
}
INSERT INTO users (id, name) VALUES (
   (1, NULL),
   (2, 'Wink')
);

Insert操作无输出。

Update
typescript
type Update = {
  'table': string;
  'row': InsertRow;
  'where'?: Where;
}
{
  "table": "users",
  "row": {"locked": true},
  "where": {
    "age": {"$gte": 60}
  }
}
UPDATE users SET locked = true WHERE age >= 60;

Update操作无输出。

Delete
typescript
type Delete = {
  'table': string;
  'where'?: Where;
}
{
  "table": "users",
  "where": {
    "age": {"$gte": "${vpt}"}
  }
}
DELETE FROM users WHERE age >= 60;

Delete操作无输出。

Raw

Raw不使用上述JSON格式,而是直接执行SQL,并按照Select的方式返回结果行。

Execute

Execute操作直接执行SQL,无输出。

三方 API

通过对第三方API进行请求,对获取到的响应值进行操作

节点配置

在业务逻辑编辑页面的节点列表中,选择 功能节点 下的 三方API 节点。

该节点支持变量使用

节点输出

自定义输出,与 HTTP 请求节点类似

生成告警

节点配置

功能节点 下找到 生成告警 节点,当 输入状态 接收到的值为 true 时,产生一条告警记录

当 输入状态 由 true 转为 false 后,自动解除告警

当状态重复输入时如 ,仅会产生一条告警

img

消息节点

MQTT 发布

与 MQTT 订阅节点功能类似,只是此节点会发布订阅

MQTT发布 的 下发数据 支持使用变量

HTTP 响应

与 HTTP 请求节点功能类似,对 HTTP 触发节点进行响应,此节点脱离 HTTP 触发节点发起的流程时不会执行。

HTTP响应节点 的 响应头(value部分) 和 响应体 支持使用变量

消息机器人 & 站内通知 & 发送邮件

以上三个节点功能大致相同,使用模板字符串,当需要使用接入值时,使用 ${变量名}来获取

消息机器人站内通知的通知内容部分支持变量使用

发送邮件的邮件部分支持变量