fix(iot): 完善场景联动配置必填校验
- Vue3 抽取场景联动校验工具,统一触发器、附加条件和执行器校验 - Vben5 antd/ele 同步场景联动提交前兜底校验 - 补充 CRON 表达式、JSON 参数和动态字段必填校验 - 保留 deviceId=0 表示全部设备的业务语义pull/351/MERGE
parent
0fe9607302
commit
c25b631c10
|
|
@ -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 不需要 param;BETWEEN_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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 不需要 param;BETWEEN_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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue