fix(iot): 完善场景联动配置必填校验

- Vue3 抽取场景联动校验工具,统一触发器、附加条件和执行器校验
- Vben5 antd/ele 同步场景联动提交前兜底校验
- 补充 CRON 表达式、JSON 参数和动态字段必填校验
- 保留 deviceId=0 表示全部设备的业务语义
pull/351/MERGE
YunaiV 2026-05-31 00:36:41 +08:00
parent 0fe9607302
commit c25b631c10
4 changed files with 844 additions and 376 deletions

View File

@ -4,15 +4,7 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene';
import { computed, nextTick, reactive, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import {
CommonStatusEnum,
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
IotRuleSceneTriggerTypeEnum,
isDeviceTrigger,
} from '@vben/constants';
import { CronUtils } from '@vben/utils';
import { CommonStatusEnum, IotRuleSceneTriggerTypeEnum } from '@vben/constants';
import { Form, message } from 'ant-design-vue';
@ -22,6 +14,10 @@ import {
updateSceneRule,
} from '#/api/iot/rule/scene';
import { $t } from '#/locales';
import {
validateSceneRuleActions,
validateSceneRuleTriggers,
} from '#/views/iot/utils/scene-rule';
import ActionSection from '../form/sections/action-section.vue';
import BasicInfoSection from '../form/sections/basic-info-section.vue';
@ -47,6 +43,16 @@ const [Drawer, drawerApi] = useVbenDrawer({
} catch {
return;
}
const triggerError = validateSceneRuleTriggers(formData.value.triggers);
if (triggerError) {
message.error(triggerError);
return;
}
const actionError = validateSceneRuleActions(formData.value.actions);
if (actionError) {
message.error(actionError);
return;
}
drawerApi.lock();
try {
const data = { ...formData.value } as RuleSceneApi.SceneRule;
@ -117,194 +123,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule {
/** 触发器校验 */
function validateTriggers(_rule: any, value: any, callback: any) {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个触发器'));
const error = validateSceneRuleTriggers(value);
if (error) {
callback(new Error(error));
return;
}
for (const [i, trigger] of value.entries()) {
if (!trigger.type) {
callback(new Error(`触发器 ${i + 1}:触发器类型不能为空`));
return;
}
if (isDeviceTrigger(trigger.type)) {
if (!trigger.productId) {
callback(new Error(`触发器 ${i + 1}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES undefined / null
if (trigger.deviceId === undefined || trigger.deviceId === null) {
callback(new Error(`触发器 ${i + 1}:设备不能为空`));
return;
}
const isStateUpdate =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE;
if (!isStateUpdate && !trigger.identifier) {
callback(new Error(`触发器 ${i + 1}:物模型标识符不能为空`));
return;
}
// / operator '=' /
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
if (!isEventOrService) {
if (!trigger.operator) {
callback(new Error(`触发器 ${i + 1}:操作符不能为空`));
return;
}
if (
trigger.value === undefined ||
trigger.value === null ||
trigger.value === ''
) {
callback(new Error(`触发器 ${i + 1}:参数值不能为空`));
return;
}
}
}
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
callback(new Error(`触发器 ${i + 1}CRON 表达式不能为空`));
return;
}
if (!CronUtils.validate(trigger.cronExpression)) {
callback(new Error(`触发器 ${i + 1}CRON 表达式格式不正确`));
return;
}
}
// conditionGroups
if (trigger.conditionGroups?.length) {
for (const [gi, group] of trigger.conditionGroups.entries()) {
if (!Array.isArray(group) || group.length === 0) {
callback(
new Error(`触发器 ${i + 1}:条件组 ${gi + 1} 不能为空`),
);
return;
}
for (const [ci, condition] of group.entries()) {
const prefix = `触发器 ${i + 1} 条件组 ${gi + 1} 条件 ${ci + 1}`;
if (!condition.type) {
callback(new Error(`${prefix}:条件类型不能为空`));
return;
}
const isDeviceStatus =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
const isDeviceProperty =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
const isCurrentTime =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
if (isDeviceStatus || isDeviceProperty) {
if (!condition.productId) {
callback(new Error(`${prefix}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES
if (
condition.deviceId === undefined ||
condition.deviceId === null
) {
callback(new Error(`${prefix}:设备不能为空`));
return;
}
if (isDeviceProperty && !condition.identifier) {
callback(new Error(`${prefix}:物模型标识符不能为空`));
return;
}
}
if (!condition.operator) {
callback(new Error(`${prefix}:操作符不能为空`));
return;
}
// param param
if (
(isDeviceStatus || isDeviceProperty) &&
(condition.param === undefined ||
condition.param === null ||
condition.param === '')
) {
callback(
new Error(
`${prefix}${isDeviceStatus ? '设备状态' : '比较值'}不能为空`,
),
);
return;
}
// TODAY paramBETWEEN_TIME v1,v2
if (isCurrentTime) {
const op = condition.operator;
if (op === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
// TODAY param
} else if (
op === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
) {
const parts = condition.param
? String(condition.param).split(',')
: [];
if (parts.length < 2 || !parts[0] || !parts[1]) {
callback(new Error(`${prefix}:起止时间不能为空`));
return;
}
} else if (!condition.param) {
callback(new Error(`${prefix}:时间值不能为空`));
return;
}
}
}
}
}
}
callback();
}
/** 执行器校验 */
function validateActions(_rule: any, value: any, callback: any) {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个执行器'));
const error = validateSceneRuleActions(value);
if (error) {
callback(new Error(error));
return;
}
for (const [i, action] of value.entries()) {
if (!action.type) {
callback(new Error(`执行器 ${i + 1}:执行器类型不能为空`));
return;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (!action.productId) {
callback(new Error(`执行器 ${i + 1}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES
// IotDevicePropertySetSceneRuleAction / IotDeviceServiceInvokeSceneRuleAction
// 广 0 undefined / null
if (action.deviceId === undefined || action.deviceId === null) {
callback(new Error(`执行器 ${i + 1}:设备不能为空`));
return;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE &&
!action.identifier
) {
callback(new Error(`执行器 ${i + 1}:服务不能为空`));
return;
}
if (!action.params || Object.keys(action.params).length === 0) {
callback(new Error(`执行器 ${i + 1}:参数配置不能为空`));
return;
}
}
// alertConfigId
if (
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
!action.alertConfigId
) {
callback(new Error(`执行器 ${i + 1}:告警配置不能为空`));
return;
}
}
callback();
}

View File

@ -0,0 +1,401 @@
import type { RuleSceneApi } from '#/api/iot/rule/scene';
import {
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
IotRuleSceneTriggerTypeEnum,
isDeviceTrigger,
} from '@vben/constants';
import { CronUtils, isEmptyVal, isObject } from '@vben/utils';
/**
* ID
*
* ID `0`
*
* @param value ID
* @returns
*/
function isRequiredIdMissing(value: unknown): boolean {
return !value;
}
/**
* ID
*
* `0`
* `undefined``null`使 falsy
*
* @param value ID
* @returns
*/
function isDeviceIdMissing(value: unknown): boolean {
return isEmptyVal(value);
}
/**
*
*
* JSON
* -
* - `{}`
* - JSON
* - JSON JSON
*
* @param params
* @returns
*/
export function isActionParamsEmpty(params?: unknown): boolean {
if (isEmptyVal(params)) {
return true;
}
if (typeof params === 'string') {
if (!params.trim()) {
return true;
}
try {
const parsed = JSON.parse(params);
if (isObject(parsed) && !Array.isArray(parsed)) {
return Object.keys(parsed).length === 0;
}
} catch {
return false;
}
return false;
}
if (isObject(params) && !Array.isArray(params)) {
return Object.keys(params).length === 0;
}
return false;
}
/**
* JSON
*
* JSON
*
* @param params
* @returns
*/
function isActionParamsJsonValid(params?: unknown): boolean {
if (isObject(params)) {
return true;
}
try {
JSON.parse(String(params));
return true;
} catch {
return false;
}
}
/**
*
*
* path便
*
*
* @param condition
* @param path
* @returns null
*/
export function validateTriggerCondition(
condition: RuleSceneApi.TriggerCondition,
path: string,
): null | string {
if (!condition.type) {
return `${path}:条件类型不能为空`;
}
const isDeviceStatus =
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
const isDeviceProperty =
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
// 设备状态和设备属性都必须先选择产品、设备deviceId = 0 表示全部设备。
if (isDeviceStatus || isDeviceProperty) {
if (isRequiredIdMissing(condition.productId)) {
return `${path}:产品不能为空`;
}
if (isDeviceIdMissing(condition.deviceId)) {
return `${path}:设备不能为空`;
}
}
// 设备状态只校验操作符和状态枚举值。
if (isDeviceStatus) {
if (!condition.operator) {
return `${path}:操作符不能为空`;
}
if (isEmptyVal(condition.param)) {
return `${path}:设备状态不能为空`;
}
return null;
}
// 设备属性需要校验物模型标识符、操作符和比较值。
if (isDeviceProperty) {
if (!condition.identifier) {
return `${path}:监控项不能为空`;
}
if (!condition.operator) {
return `${path}:操作符不能为空`;
}
if (isEmptyVal(condition.param)) {
return `${path}:比较值不能为空`;
}
return null;
}
// 当前时间按操作符动态判断 param 是否需要填写。
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
if (!condition.operator) {
return `${path}:时间条件不能为空`;
}
if (
condition.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value
) {
return null;
}
if (isEmptyVal(condition.param)) {
return `${path}:时间值不能为空`;
}
if (
condition.operator ===
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
) {
const parts = String(condition.param).split(',');
if (!parts[0]?.trim() || !parts[1]?.trim()) {
return `${path}:开始和结束时间不能为空`;
}
}
}
return null;
}
/**
*
*
* OR AND
*
*
* @param groups
* @param triggerIndex
* @returns null
*/
export function validateTriggerConditionGroups(
groups: RuleSceneApi.TriggerCondition[][] | undefined,
triggerIndex: number,
): null | string {
if (!groups?.length) {
return null;
}
for (const [groupIndex, group] of groups.entries()) {
// 空条件组没有实际过滤条件,提交后语义不明确,需要拦截。
if (!Array.isArray(group) || group.length === 0) {
return `触发器 ${triggerIndex + 1}:条件组 ${groupIndex + 1} 不能为空`;
}
for (const [conditionIndex, condition] of group.entries()) {
const error = validateTriggerCondition(
condition,
`触发器 ${triggerIndex + 1} 条件组 ${groupIndex + 1} 条件 ${
conditionIndex + 1
}`,
);
if (error) {
return error;
}
}
}
return null;
}
/**
*
*
* prop
* UI
*
* @param trigger
* @param index
* @returns null
*/
export function validateTriggerItem(
trigger: RuleSceneApi.Trigger,
index: number,
): null | string {
const prefix = `触发器 ${index + 1}`;
if (!trigger.type) {
return `${prefix}:触发器类型不能为空`;
}
// 设备类触发器都有产品、设备两个基础字段deviceId = 0 表示全部设备。
if (isDeviceTrigger(trigger.type)) {
if (isRequiredIdMissing(trigger.productId)) {
return `${prefix}:产品不能为空`;
}
if (isDeviceIdMissing(trigger.deviceId)) {
return `${prefix}:设备不能为空`;
}
// 设备状态变化不依赖物模型标识符,只校验操作符和状态值。
if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
if (!trigger.operator) {
return `${prefix}:操作符不能为空`;
}
if (isEmptyVal(trigger.value)) {
return `${prefix}:设备状态不能为空`;
}
} else {
if (!trigger.identifier) {
return `${prefix}:监控项不能为空`;
}
// 事件上报和服务调用只监听是否发生,不需要额外的操作符和比较值。
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
if (!isEventOrService) {
if (!trigger.operator) {
return `${prefix}:操作符不能为空`;
}
if (isEmptyVal(trigger.value)) {
return `${prefix}:参数值不能为空`;
}
}
}
}
// 定时触发器需要 CRON 表达式,并继续校验 CRON 格式。
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
return `${prefix}CRON 表达式不能为空`;
}
if (!CronUtils.validate(trigger.cronExpression)) {
return `${prefix}CRON 表达式格式不正确`;
}
}
return validateTriggerConditionGroups(trigger.conditionGroups, index);
}
/**
*
*
*
*
* @param triggers
* @returns null
*/
export function validateSceneRuleTriggers(
triggers?: RuleSceneApi.Trigger[],
): null | string {
if (!triggers?.length) {
return '至少需要一个触发器';
}
for (const [index, trigger] of triggers.entries()) {
const error = validateTriggerItem(trigger, index);
if (error) {
return error;
}
}
return null;
}
/**
*
*
* prop
* UI
*
* @param action
* @param index
* @returns null
*/
export function validateActionItem(
action: RuleSceneApi.Action,
index: number,
): null | string {
const prefix = `执行器 ${index + 1}`;
if (!action.type) {
return `${prefix}:执行器类型不能为空`;
}
// 设备属性设置和设备服务调用都需要指定设备,并填写物模型参数。
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (isRequiredIdMissing(action.productId)) {
return `${prefix}:产品不能为空`;
}
if (isDeviceIdMissing(action.deviceId)) {
return `${prefix}:设备不能为空`;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE &&
!action.identifier
) {
return `${prefix}:服务不能为空`;
}
if (isActionParamsEmpty(action.params)) {
return `${prefix}:参数配置不能为空`;
}
if (!isActionParamsJsonValid(action.params)) {
return `${prefix}:参数格式须为合法 JSON`;
}
return null;
}
// 告警恢复执行器需要绑定具体告警配置;触发告警不需要预选告警配置。
if (
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
!action.alertConfigId
) {
return `${prefix}:告警配置不能为空`;
}
return null;
}
/**
*
*
*
*
* @param actions
* @returns null
*/
export function validateSceneRuleActions(
actions?: RuleSceneApi.Action[],
): null | string {
if (!actions?.length) {
return '至少需要一个执行器';
}
for (const [index, action] of actions.entries()) {
const error = validateActionItem(action, index);
if (error) {
return error;
}
}
return null;
}

View File

@ -4,15 +4,7 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene';
import { computed, nextTick, reactive, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import {
CommonStatusEnum,
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
IotRuleSceneTriggerTypeEnum,
isDeviceTrigger,
} from '@vben/constants';
import { CronUtils } from '@vben/utils';
import { CommonStatusEnum, IotRuleSceneTriggerTypeEnum } from '@vben/constants';
import { ElForm, ElMessage } from 'element-plus';
@ -22,6 +14,10 @@ import {
updateSceneRule,
} from '#/api/iot/rule/scene';
import { $t } from '#/locales';
import {
validateSceneRuleActions,
validateSceneRuleTriggers,
} from '#/views/iot/utils/scene-rule';
import ActionSection from '../form/sections/action-section.vue';
import BasicInfoSection from '../form/sections/basic-info-section.vue';
@ -47,6 +43,16 @@ const [Drawer, drawerApi] = useVbenDrawer({
} catch {
return;
}
const triggerError = validateSceneRuleTriggers(formData.value.triggers);
if (triggerError) {
ElMessage.error(triggerError);
return;
}
const actionError = validateSceneRuleActions(formData.value.actions);
if (actionError) {
ElMessage.error(actionError);
return;
}
drawerApi.lock();
try {
const data = { ...formData.value } as RuleSceneApi.SceneRule;
@ -117,194 +123,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule {
/** 触发器校验 */
function validateTriggers(_rule: any, value: any, callback: any) {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个触发器'));
const error = validateSceneRuleTriggers(value);
if (error) {
callback(new Error(error));
return;
}
for (const [i, trigger] of value.entries()) {
if (!trigger.type) {
callback(new Error(`触发器 ${i + 1}:触发器类型不能为空`));
return;
}
if (isDeviceTrigger(trigger.type)) {
if (!trigger.productId) {
callback(new Error(`触发器 ${i + 1}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES undefined / null
if (trigger.deviceId === undefined || trigger.deviceId === null) {
callback(new Error(`触发器 ${i + 1}:设备不能为空`));
return;
}
const isStateUpdate =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE;
if (!isStateUpdate && !trigger.identifier) {
callback(new Error(`触发器 ${i + 1}:物模型标识符不能为空`));
return;
}
// / operator '=' /
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
if (!isEventOrService) {
if (!trigger.operator) {
callback(new Error(`触发器 ${i + 1}:操作符不能为空`));
return;
}
if (
trigger.value === undefined ||
trigger.value === null ||
trigger.value === ''
) {
callback(new Error(`触发器 ${i + 1}:参数值不能为空`));
return;
}
}
}
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
callback(new Error(`触发器 ${i + 1}CRON 表达式不能为空`));
return;
}
if (!CronUtils.validate(trigger.cronExpression)) {
callback(new Error(`触发器 ${i + 1}CRON 表达式格式不正确`));
return;
}
}
// conditionGroups
if (trigger.conditionGroups?.length) {
for (const [gi, group] of trigger.conditionGroups.entries()) {
if (!Array.isArray(group) || group.length === 0) {
callback(
new Error(`触发器 ${i + 1}:条件组 ${gi + 1} 不能为空`),
);
return;
}
for (const [ci, condition] of group.entries()) {
const prefix = `触发器 ${i + 1} 条件组 ${gi + 1} 条件 ${ci + 1}`;
if (!condition.type) {
callback(new Error(`${prefix}:条件类型不能为空`));
return;
}
const isDeviceStatus =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
const isDeviceProperty =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
const isCurrentTime =
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
if (isDeviceStatus || isDeviceProperty) {
if (!condition.productId) {
callback(new Error(`${prefix}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES
if (
condition.deviceId === undefined ||
condition.deviceId === null
) {
callback(new Error(`${prefix}:设备不能为空`));
return;
}
if (isDeviceProperty && !condition.identifier) {
callback(new Error(`${prefix}:物模型标识符不能为空`));
return;
}
}
if (!condition.operator) {
callback(new Error(`${prefix}:操作符不能为空`));
return;
}
// param param
if (
(isDeviceStatus || isDeviceProperty) &&
(condition.param === undefined ||
condition.param === null ||
condition.param === '')
) {
callback(
new Error(
`${prefix}${isDeviceStatus ? '设备状态' : '比较值'}不能为空`,
),
);
return;
}
// TODAY paramBETWEEN_TIME v1,v2
if (isCurrentTime) {
const op = condition.operator;
if (op === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
// TODAY param
} else if (
op === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
) {
const parts = condition.param
? String(condition.param).split(',')
: [];
if (parts.length < 2 || !parts[0] || !parts[1]) {
callback(new Error(`${prefix}:起止时间不能为空`));
return;
}
} else if (!condition.param) {
callback(new Error(`${prefix}:时间值不能为空`));
return;
}
}
}
}
}
}
callback();
}
/** 执行器校验 */
function validateActions(_rule: any, value: any, callback: any) {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个执行器'));
const error = validateSceneRuleActions(value);
if (error) {
callback(new Error(error));
return;
}
for (const [i, action] of value.entries()) {
if (!action.type) {
callback(new Error(`执行器 ${i + 1}:执行器类型不能为空`));
return;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (!action.productId) {
callback(new Error(`执行器 ${i + 1}:产品不能为空`));
return;
}
// deviceId = 0 DEVICE_SELECTOR_OPTIONS.ALL_DEVICES
// IotDevicePropertySetSceneRuleAction / IotDeviceServiceInvokeSceneRuleAction
// 广 0 undefined / null
if (action.deviceId === undefined || action.deviceId === null) {
callback(new Error(`执行器 ${i + 1}:设备不能为空`));
return;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE &&
!action.identifier
) {
callback(new Error(`执行器 ${i + 1}:服务不能为空`));
return;
}
if (!action.params || Object.keys(action.params).length === 0) {
callback(new Error(`执行器 ${i + 1}:参数配置不能为空`));
return;
}
}
// alertConfigId
if (
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
!action.alertConfigId
) {
callback(new Error(`执行器 ${i + 1}:告警配置不能为空`));
return;
}
}
callback();
}

View File

@ -0,0 +1,401 @@
import type { RuleSceneApi } from '#/api/iot/rule/scene';
import {
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
IotRuleSceneTriggerTypeEnum,
isDeviceTrigger,
} from '@vben/constants';
import { CronUtils, isEmptyVal, isObject } from '@vben/utils';
/**
* ID
*
* ID `0`
*
* @param value ID
* @returns
*/
function isRequiredIdMissing(value: unknown): boolean {
return !value;
}
/**
* ID
*
* `0`
* `undefined``null`使 falsy
*
* @param value ID
* @returns
*/
function isDeviceIdMissing(value: unknown): boolean {
return isEmptyVal(value);
}
/**
*
*
* JSON
* -
* - `{}`
* - JSON
* - JSON JSON
*
* @param params
* @returns
*/
export function isActionParamsEmpty(params?: unknown): boolean {
if (isEmptyVal(params)) {
return true;
}
if (typeof params === 'string') {
if (!params.trim()) {
return true;
}
try {
const parsed = JSON.parse(params);
if (isObject(parsed) && !Array.isArray(parsed)) {
return Object.keys(parsed).length === 0;
}
} catch {
return false;
}
return false;
}
if (isObject(params) && !Array.isArray(params)) {
return Object.keys(params).length === 0;
}
return false;
}
/**
* JSON
*
* JSON
*
* @param params
* @returns
*/
function isActionParamsJsonValid(params?: unknown): boolean {
if (isObject(params)) {
return true;
}
try {
JSON.parse(String(params));
return true;
} catch {
return false;
}
}
/**
*
*
* path便
*
*
* @param condition
* @param path
* @returns null
*/
export function validateTriggerCondition(
condition: RuleSceneApi.TriggerCondition,
path: string,
): null | string {
if (!condition.type) {
return `${path}:条件类型不能为空`;
}
const isDeviceStatus =
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
const isDeviceProperty =
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY;
// 设备状态和设备属性都必须先选择产品、设备deviceId = 0 表示全部设备。
if (isDeviceStatus || isDeviceProperty) {
if (isRequiredIdMissing(condition.productId)) {
return `${path}:产品不能为空`;
}
if (isDeviceIdMissing(condition.deviceId)) {
return `${path}:设备不能为空`;
}
}
// 设备状态只校验操作符和状态枚举值。
if (isDeviceStatus) {
if (!condition.operator) {
return `${path}:操作符不能为空`;
}
if (isEmptyVal(condition.param)) {
return `${path}:设备状态不能为空`;
}
return null;
}
// 设备属性需要校验物模型标识符、操作符和比较值。
if (isDeviceProperty) {
if (!condition.identifier) {
return `${path}:监控项不能为空`;
}
if (!condition.operator) {
return `${path}:操作符不能为空`;
}
if (isEmptyVal(condition.param)) {
return `${path}:比较值不能为空`;
}
return null;
}
// 当前时间按操作符动态判断 param 是否需要填写。
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
if (!condition.operator) {
return `${path}:时间条件不能为空`;
}
if (
condition.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value
) {
return null;
}
if (isEmptyVal(condition.param)) {
return `${path}:时间值不能为空`;
}
if (
condition.operator ===
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
) {
const parts = String(condition.param).split(',');
if (!parts[0]?.trim() || !parts[1]?.trim()) {
return `${path}:开始和结束时间不能为空`;
}
}
}
return null;
}
/**
*
*
* OR AND
*
*
* @param groups
* @param triggerIndex
* @returns null
*/
export function validateTriggerConditionGroups(
groups: RuleSceneApi.TriggerCondition[][] | undefined,
triggerIndex: number,
): null | string {
if (!groups?.length) {
return null;
}
for (const [groupIndex, group] of groups.entries()) {
// 空条件组没有实际过滤条件,提交后语义不明确,需要拦截。
if (!Array.isArray(group) || group.length === 0) {
return `触发器 ${triggerIndex + 1}:条件组 ${groupIndex + 1} 不能为空`;
}
for (const [conditionIndex, condition] of group.entries()) {
const error = validateTriggerCondition(
condition,
`触发器 ${triggerIndex + 1} 条件组 ${groupIndex + 1} 条件 ${
conditionIndex + 1
}`,
);
if (error) {
return error;
}
}
}
return null;
}
/**
*
*
* prop
* UI
*
* @param trigger
* @param index
* @returns null
*/
export function validateTriggerItem(
trigger: RuleSceneApi.Trigger,
index: number,
): null | string {
const prefix = `触发器 ${index + 1}`;
if (!trigger.type) {
return `${prefix}:触发器类型不能为空`;
}
// 设备类触发器都有产品、设备两个基础字段deviceId = 0 表示全部设备。
if (isDeviceTrigger(trigger.type)) {
if (isRequiredIdMissing(trigger.productId)) {
return `${prefix}:产品不能为空`;
}
if (isDeviceIdMissing(trigger.deviceId)) {
return `${prefix}:设备不能为空`;
}
// 设备状态变化不依赖物模型标识符,只校验操作符和状态值。
if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
if (!trigger.operator) {
return `${prefix}:操作符不能为空`;
}
if (isEmptyVal(trigger.value)) {
return `${prefix}:设备状态不能为空`;
}
} else {
if (!trigger.identifier) {
return `${prefix}:监控项不能为空`;
}
// 事件上报和服务调用只监听是否发生,不需要额外的操作符和比较值。
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
if (!isEventOrService) {
if (!trigger.operator) {
return `${prefix}:操作符不能为空`;
}
if (isEmptyVal(trigger.value)) {
return `${prefix}:参数值不能为空`;
}
}
}
}
// 定时触发器需要 CRON 表达式,并继续校验 CRON 格式。
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
return `${prefix}CRON 表达式不能为空`;
}
if (!CronUtils.validate(trigger.cronExpression)) {
return `${prefix}CRON 表达式格式不正确`;
}
}
return validateTriggerConditionGroups(trigger.conditionGroups, index);
}
/**
*
*
*
*
* @param triggers
* @returns null
*/
export function validateSceneRuleTriggers(
triggers?: RuleSceneApi.Trigger[],
): null | string {
if (!triggers?.length) {
return '至少需要一个触发器';
}
for (const [index, trigger] of triggers.entries()) {
const error = validateTriggerItem(trigger, index);
if (error) {
return error;
}
}
return null;
}
/**
*
*
* prop
* UI
*
* @param action
* @param index
* @returns null
*/
export function validateActionItem(
action: RuleSceneApi.Action,
index: number,
): null | string {
const prefix = `执行器 ${index + 1}`;
if (!action.type) {
return `${prefix}:执行器类型不能为空`;
}
// 设备属性设置和设备服务调用都需要指定设备,并填写物模型参数。
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (isRequiredIdMissing(action.productId)) {
return `${prefix}:产品不能为空`;
}
if (isDeviceIdMissing(action.deviceId)) {
return `${prefix}:设备不能为空`;
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE &&
!action.identifier
) {
return `${prefix}:服务不能为空`;
}
if (isActionParamsEmpty(action.params)) {
return `${prefix}:参数配置不能为空`;
}
if (!isActionParamsJsonValid(action.params)) {
return `${prefix}:参数格式须为合法 JSON`;
}
return null;
}
// 告警恢复执行器需要绑定具体告警配置;触发告警不需要预选告警配置。
if (
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER &&
!action.alertConfigId
) {
return `${prefix}:告警配置不能为空`;
}
return null;
}
/**
*
*
*
*
* @param actions
* @returns null
*/
export function validateSceneRuleActions(
actions?: RuleSceneApi.Action[],
): null | string {
if (!actions?.length) {
return '至少需要一个执行器';
}
for (const [index, action] of actions.entries()) {
const error = validateActionItem(action, index);
if (error) {
return error;
}
}
return null;
}