|
|
@@ -4,6 +4,7 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
|
|
|
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
|
|
|
+using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
|
|
|
@@ -13,17 +14,20 @@ public class S8WatchRuleService : ITransient
|
|
|
private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
|
|
|
private readonly IS8DatasetCatalog _datasetCatalog;
|
|
|
private readonly S8DatasetEnableGate _datasetEnableGate;
|
|
|
+ private readonly IS8RuleCatalog _ruleCatalog;
|
|
|
|
|
|
public S8WatchRuleService(
|
|
|
SqlSugarRepository<AdoS8WatchRule> rep,
|
|
|
SqlSugarRepository<AdoS8SceneConfig> sceneRep,
|
|
|
IS8DatasetCatalog datasetCatalog,
|
|
|
- S8DatasetEnableGate datasetEnableGate)
|
|
|
+ S8DatasetEnableGate datasetEnableGate,
|
|
|
+ IS8RuleCatalog ruleCatalog)
|
|
|
{
|
|
|
_rep = rep;
|
|
|
_sceneRep = sceneRep;
|
|
|
_datasetCatalog = datasetCatalog;
|
|
|
_datasetEnableGate = datasetEnableGate;
|
|
|
+ _ruleCatalog = ruleCatalog;
|
|
|
}
|
|
|
|
|
|
public async Task<List<AdoS8WatchRule>> ListAsync(long tenantId, long factoryId) =>
|
|
|
@@ -44,7 +48,7 @@ public class S8WatchRuleService : ITransient
|
|
|
// S8-STEP6E:新建一律走 canonical 词表 + params schema,杜绝「存得下但运行时永远不生效」。
|
|
|
ValidateVocabularyForCreate(body);
|
|
|
if (!string.IsNullOrWhiteSpace(body.ParamsJson))
|
|
|
- ValidateParamsJsonByRuleType(body.RuleType, body.ParamsJson!.Trim());
|
|
|
+ ValidateParamsJsonShape(body.ParamsJson!.Trim());
|
|
|
body.Id = 0;
|
|
|
body.CreatedAt = DateTime.Now;
|
|
|
// S8-STEP6E(与 CFG_DATASRC D-3 / CFG_ROLES 同源缺陷):回填自增主键。
|
|
|
@@ -100,43 +104,177 @@ public class S8WatchRuleService : ITransient
|
|
|
.FirstAsync() ?? throw new S8NotFoundException();
|
|
|
|
|
|
/// <summary>
|
|
|
- /// R4 安全更新:只更新 params_json 与 enabled。expression / rule_code / data_source_id /
|
|
|
- /// scene_code / watch_object_type / rule_type / source_object_type 一律不通过此路径修改。
|
|
|
- /// 当 RuleType 非空时,按对应 evaluator 的 Params.Parse 进行 schema 校验,解析失败抛 S8BizException。
|
|
|
+ /// S8-RULE-GOVERNANCE-BATCH1:运行参数的**部分更新**。
|
|
|
+ ///
|
|
|
+ /// <para>与被它取代的 <c>UpdateParamsAsync</c> 的三处根本差异:</para>
|
|
|
+ /// <list type="number">
|
|
|
+ /// <item><b>不再接受 params_json 原文</b>。判定语义(dueAtField / statusField /
|
|
|
+ /// completedStates / objectIdField / exceptionTypeCode)现在只存在于代码定义里,
|
|
|
+ /// 没有任何 API 能改到它们;</item>
|
|
|
+ /// <item><b>不再承担启停</b>。enabled 走 <see cref="EnableAsync"/> / <see cref="DisableAsync"/>;</item>
|
|
|
+ /// <item><b>PATCH 而非整块覆盖</b>。未提供的字段保持原值 —— 这是 G1 的直接修复:
|
|
|
+ /// 旧实现里一次 <c>{"enabled":false}</c> 就会把 params_json 抹成 NULL。</item>
|
|
|
+ /// </list>
|
|
|
+ ///
|
|
|
+ /// <para>写入用 <c>UpdateColumns</c> 白名单,物理上无法触碰 Definition 投影列与 13 个调度运行态列。</para>
|
|
|
/// </summary>
|
|
|
- public async Task<AdoS8WatchRule> UpdateParamsAsync(long id, S8WatchRuleParamsPayload payload, S8TrustedScope scope)
|
|
|
+ public async Task<AdoS8WatchRule> UpdateParametersAsync(long id, S8RuleParametersPayload payload, S8TrustedScope scope)
|
|
|
{
|
|
|
+ // 前置拒绝:在任何 DB 访问之前判掉非法载荷,越权 id 也不会被用来探测记录是否存在。
|
|
|
+ // 同一道守卫在 ApplyParameters 内再做一次 —— 那里才是所有调用方的必经之处。
|
|
|
+ EnsureNoDefinitionFields(payload);
|
|
|
+
|
|
|
var entity = await LoadScopedAsync(id, scope);
|
|
|
+ var definition = _ruleCatalog.GetRequired(entity.RuleCode);
|
|
|
+
|
|
|
+ var next = ApplyParameters(entity, payload, definition);
|
|
|
+
|
|
|
+ await _rep.Context.Updateable<AdoS8WatchRule>()
|
|
|
+ .SetColumns(x => new AdoS8WatchRule
|
|
|
+ {
|
|
|
+ PollIntervalSeconds = next.PollIntervalSeconds,
|
|
|
+ TriggerCountRequired = next.TriggerCountRequired,
|
|
|
+ RecoverCountRequired = next.RecoverCountRequired,
|
|
|
+ Severity = next.Severity,
|
|
|
+ ParamsJson = next.ToParamsJson(),
|
|
|
+ UpdatedAt = DateTime.Now
|
|
|
+ })
|
|
|
+ .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ return await LoadScopedAsync(id, scope);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 纯函数:把 PATCH 载荷叠加到「当前生效参数」上,并按 Definition 声明的取值域校验。
|
|
|
+ ///
|
|
|
+ /// <para>抽成 static 是为了让 G1 的核心不变量(未提供 = 不变)能在**不接数据库**的情况下被测试
|
|
|
+ /// 逐字段断言。之前那条缺陷之所以能活到生产,正是因为它藏在一个必须有仓储才能跑的方法里。</para>
|
|
|
+ /// </summary>
|
|
|
+ internal static S8RuleRuntimeParameters ApplyParameters(
|
|
|
+ AdoS8WatchRule entity, S8RuleParametersPayload payload, S8RuleDefinition definition)
|
|
|
+ {
|
|
|
+ EnsureNoDefinitionFields(payload);
|
|
|
+
|
|
|
+ var current = S8RuleRuntimeParameters.Resolve(entity, definition);
|
|
|
+ var policy = definition.Parameters ?? new S8RuleParameterPolicy();
|
|
|
+
|
|
|
+ var poll = payload.PollIntervalSeconds ?? current.PollIntervalSeconds;
|
|
|
+ var trigger = payload.TriggerCountRequired ?? current.TriggerCountRequired;
|
|
|
+ var recover = payload.RecoverCountRequired ?? current.RecoverCountRequired;
|
|
|
+ var grace = payload.GraceMinutes ?? current.GraceMinutes;
|
|
|
+ var severity = payload.Severity?.Trim() ?? current.Severity;
|
|
|
+
|
|
|
+ EnsureInRange("轮询间隔(秒)", poll, policy.PollIntervalSecondsMin, policy.PollIntervalSecondsMax);
|
|
|
+ EnsureInRange("连续命中建单次数", trigger, policy.TriggerCountRequiredMin, policy.TriggerCountRequiredMax);
|
|
|
+ EnsureInRange("连续未命中恢复次数", recover, policy.RecoverCountRequiredMin, policy.RecoverCountRequiredMax);
|
|
|
+ EnsureInRange("宽限分钟", grace, policy.GraceMinutesMin, policy.GraceMinutesMax);
|
|
|
+
|
|
|
+ if (!policy.AllowedSeverities.Contains(severity, StringComparer.Ordinal))
|
|
|
+ throw new S8BizException(
|
|
|
+ $"不支持的严重度:{severity};当前规则仅支持 {string.Join(" / ", policy.AllowedSeverities)}");
|
|
|
+
|
|
|
+ var occurrenceDept = payload.DefaultOccurrenceDeptId ?? current.DefaultOccurrenceDeptId;
|
|
|
+ var responsibleDept = payload.DefaultResponsibleDeptId ?? current.DefaultResponsibleDeptId;
|
|
|
+ if (!policy.AllowsDepartmentDefaults && (occurrenceDept.HasValue || responsibleDept.HasValue))
|
|
|
+ throw new S8BizException("当前规则不支持配置部门兜底");
|
|
|
|
|
|
- var paramsJson = payload.ParamsJson?.Trim();
|
|
|
- if (!string.IsNullOrEmpty(paramsJson))
|
|
|
+ return new S8RuleRuntimeParameters
|
|
|
{
|
|
|
- ValidateParamsJsonByRuleType(entity.RuleType, paramsJson);
|
|
|
- }
|
|
|
+ PollIntervalSeconds = poll,
|
|
|
+ TriggerCountRequired = trigger,
|
|
|
+ RecoverCountRequired = recover,
|
|
|
+ Severity = severity,
|
|
|
+ GraceMinutes = grace,
|
|
|
+ DefaultOccurrenceDeptId = occurrenceDept,
|
|
|
+ DefaultResponsibleDeptId = responsibleDept
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 载荷里出现 Definition 字段(或已迁走的 enabled)一律**显式拒绝**。
|
|
|
+ ///
|
|
|
+ /// <para>静默忽略等于告诉调用方"改成功了",而实际什么都没发生 —— 那比报错更危险,
|
|
|
+ /// 因为调用方会据此认为规则已经按新口径运行。</para>
|
|
|
+ ///
|
|
|
+ /// <para>放在 <see cref="ApplyParameters"/> 内而不是只放在 API 层:
|
|
|
+ /// 纯函数是所有写入路径的必经之处,守卫挂在这里才不会被下一个调用方绕过。</para>
|
|
|
+ /// </summary>
|
|
|
+ private static void EnsureNoDefinitionFields(S8RuleParametersPayload payload)
|
|
|
+ {
|
|
|
+ if (payload == null) throw new S8BizException("请求体不能为空");
|
|
|
+
|
|
|
+ var rejected = payload.RejectedDefinitionFields;
|
|
|
+ if (rejected.Count > 0)
|
|
|
+ throw new S8BizException(
|
|
|
+ "以下字段由代码定义,不能通过参数接口修改:" + string.Join(" / ", rejected)
|
|
|
+ + ";启停请使用 /enable 与 /disable");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void EnsureInRange(string label, int value, int min, int max)
|
|
|
+ {
|
|
|
+ if (value < min || value > max)
|
|
|
+ throw new S8BizException($"{label} 必须在 {min}–{max} 之间,当前值 {value}");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 启用规则。<b>只写 enabled 与调度触发时间,绝不触碰任何参数列或 Definition 投影列。</b>
|
|
|
+ ///
|
|
|
+ /// <para>幂等:已启用时直接返回,不产生写入 —— 重复调用不会重排下次执行时间,
|
|
|
+ /// 也就不会被用来变相"插队"调度。</para>
|
|
|
+ /// </summary>
|
|
|
+ public async Task<AdoS8WatchRule> EnableAsync(long id, S8TrustedScope scope)
|
|
|
+ {
|
|
|
+ var entity = await LoadScopedAsync(id, scope);
|
|
|
+ if (entity.Enabled) return entity;
|
|
|
+
|
|
|
+ // ① 没有代码定义的规则不得启用。这是 Create API 仍然存在期间的安全过渡:
|
|
|
+ // 业务即使造出一条任意 rule_code 的规则,也无法让调度器替它跑。
|
|
|
+ var definition = _ruleCatalog.GetRequired(entity.RuleCode);
|
|
|
+
|
|
|
+ // ② 数据集侧完整运行条件。按 Definition 的 dataset_code / rule_type 判定,
|
|
|
+ // 而不是 DB 上那两列 —— 后者是投影,可能被人为改过。
|
|
|
+ _datasetEnableGate.EnsureCanEnable(
|
|
|
+ definition.DatasetCode, definition.RuleType, definition.RuleCode, scope.TenantId, scope.FactoryId);
|
|
|
|
|
|
- entity.ParamsJson = string.IsNullOrEmpty(paramsJson) ? null : paramsJson;
|
|
|
-
|
|
|
- // S8-DATASET-FOUNDATION-HARDENING-2:本方法是规则 enabled 的唯一切换入口,
|
|
|
- // 因此 Enable Gate 挂在此处。由 disabled → enabled 时必须满足完整运行条件;
|
|
|
- // 关闭规则不受 Gate 约束(否则数据集出问题后规则将无法被关停)。
|
|
|
- if (payload.Enabled && !entity.Enabled)
|
|
|
- S8WatchRuleDataAccessValidator.ValidateForEnable(entity, _datasetEnableGate, scope.TenantId, scope.FactoryId);
|
|
|
-
|
|
|
- entity.Enabled = payload.Enabled;
|
|
|
- // TASK-002-RESET-DIMENSION-MODEL-DEV-2B:维度归属 + 报警机制按 payload 原样落库(含 null 清空)。
|
|
|
- entity.StageCode = NormalizeOrNull(payload.StageCode);
|
|
|
- entity.OrderFlowCode = NormalizeOrNull(payload.OrderFlowCode);
|
|
|
- entity.RuleMechanism = NormalizeOrNull(payload.RuleMechanism);
|
|
|
- entity.UpdatedAt = DateTime.Now;
|
|
|
- await _rep.UpdateAsync(entity);
|
|
|
- return entity;
|
|
|
+ var now = DateTime.Now;
|
|
|
+ await _rep.Context.Updateable<AdoS8WatchRule>()
|
|
|
+ .SetColumns(x => new AdoS8WatchRule
|
|
|
+ {
|
|
|
+ Enabled = true,
|
|
|
+ NextRunAt = now,
|
|
|
+ UpdatedAt = now
|
|
|
+ })
|
|
|
+ .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ return await LoadScopedAsync(id, scope);
|
|
|
}
|
|
|
|
|
|
- private static string? NormalizeOrNull(string? value)
|
|
|
+ /// <summary>
|
|
|
+ /// 停用规则。<b>只写 enabled。</b>
|
|
|
+ ///
|
|
|
+ /// <para>幂等:已停用时直接返回。</para>
|
|
|
+ /// <para>不动 lease / next_run_at:<c>PickReadyRulesAsync</c> 的候选谓词第一条就是
|
|
|
+ /// <c>x.Enabled</c>,停用后自然不会再被拾取;正在执行中的那一轮由
|
|
|
+ /// <c>ResetExpiredLeasesAsync</c> 按既有租约语义收尾。强行清租约反而会与正在跑的实例撕扯。</para>
|
|
|
+ /// </summary>
|
|
|
+ public async Task<AdoS8WatchRule> DisableAsync(long id, S8TrustedScope scope)
|
|
|
{
|
|
|
- if (string.IsNullOrWhiteSpace(value)) return null;
|
|
|
- var trimmed = value.Trim();
|
|
|
- return trimmed.Length == 0 ? null : trimmed;
|
|
|
+ var entity = await LoadScopedAsync(id, scope);
|
|
|
+ if (!entity.Enabled) return entity;
|
|
|
+
|
|
|
+ // 停用**不过** Enable Gate:数据集出问题之后仍然必须能把规则关掉。
|
|
|
+ await _rep.Context.Updateable<AdoS8WatchRule>()
|
|
|
+ .SetColumns(x => new AdoS8WatchRule
|
|
|
+ {
|
|
|
+ Enabled = false,
|
|
|
+ UpdatedAt = DateTime.Now
|
|
|
+ })
|
|
|
+ .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ return await LoadScopedAsync(id, scope);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -188,47 +326,20 @@ public class S8WatchRuleService : ITransient
|
|
|
+ S8SeverityCode.Follow + " / " + S8SeverityCode.Serious);
|
|
|
}
|
|
|
|
|
|
- private static void ValidateParamsJsonByRuleType(string? ruleType, string paramsJson)
|
|
|
+ /// <summary>
|
|
|
+ /// S8-RULE-GOVERNANCE-BATCH1:params_json 的按类型 schema 校验已退役。
|
|
|
+ ///
|
|
|
+ /// <para>它校验的是 <c>dueAtField</c> / <c>statusField</c> / <c>exceptionTypeCode</c> 这些
|
|
|
+ /// **A 类判定语义**是否齐备 —— 而这些字段现在只存在于代码定义里,params_json 根本不再承载它们。
|
|
|
+ /// 继续校验等于要求调用方提交一份已经没有意义的 JSON。</para>
|
|
|
+ ///
|
|
|
+ /// <para>保留 JSON 合法性检查:脏文本存进去会让运行期解析失败,
|
|
|
+ /// 虽然 <see cref="S8RuleRuntimeParameters.Resolve"/> 会回落默认值而不报错,
|
|
|
+ /// 但让一份读不懂的文本静静躺在库里没有任何好处。</para>
|
|
|
+ /// </summary>
|
|
|
+ private static void ValidateParamsJsonShape(string paramsJson)
|
|
|
{
|
|
|
- try
|
|
|
- {
|
|
|
- switch (ruleType)
|
|
|
- {
|
|
|
- case S8TimeoutRuleEvaluator.RuleTypeCode:
|
|
|
- {
|
|
|
- var p = S8TimeoutParams.Parse(paramsJson);
|
|
|
- if (string.IsNullOrWhiteSpace(p.DueAtField)
|
|
|
- || string.IsNullOrWhiteSpace(p.StatusField)
|
|
|
- || string.IsNullOrWhiteSpace(p.ExceptionTypeCode))
|
|
|
- throw new S8BizException("TIMEOUT params 缺少必填字段:dueAtField / statusField / exceptionTypeCode");
|
|
|
- break;
|
|
|
- }
|
|
|
- case S8ShortageRuleEvaluator.RuleTypeCode:
|
|
|
- {
|
|
|
- var p = S8ShortageParams.Parse(paramsJson);
|
|
|
- if (string.IsNullOrWhiteSpace(p.TargetQtyField)
|
|
|
- || string.IsNullOrWhiteSpace(p.ActualQtyField)
|
|
|
- || string.IsNullOrWhiteSpace(p.ExceptionTypeCode))
|
|
|
- throw new S8BizException("SHORTAGE params 缺少必填字段:targetQtyField / actualQtyField / exceptionTypeCode");
|
|
|
- break;
|
|
|
- }
|
|
|
- case S8OutOfRangeRuleEvaluator.RuleTypeCode:
|
|
|
- {
|
|
|
- var p = S8OutOfRangeParams.Parse(paramsJson);
|
|
|
- if (string.IsNullOrWhiteSpace(p.MeasuredValueField))
|
|
|
- throw new S8BizException("OUT_OF_RANGE params 缺少必填字段:measuredValueField");
|
|
|
- if (p.LowerBound == null && p.UpperBound == null
|
|
|
- && string.IsNullOrWhiteSpace(p.LowerBoundField)
|
|
|
- && string.IsNullOrWhiteSpace(p.UpperBoundField))
|
|
|
- throw new S8BizException("OUT_OF_RANGE params 必须提供 upperBound / lowerBound 或对应行内字段之一");
|
|
|
- break;
|
|
|
- }
|
|
|
- default:
|
|
|
- // RuleType 为空或非三类已知值:仅做 JSON 合法性校验,避免阻塞历史数据。
|
|
|
- using (JsonDocument.Parse(paramsJson)) { }
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ try { using (JsonDocument.Parse(paramsJson)) { } }
|
|
|
catch (JsonException ex)
|
|
|
{
|
|
|
throw new S8BizException($"params_json 不是合法 JSON:{ex.Message}");
|
|
|
@@ -249,29 +360,15 @@ public class S8WatchRuleService : ITransient
|
|
|
/// S8-SCHED-FRONTEND-1:调度参数安全更新。仅修改 poll_interval_seconds / trigger_count_required /
|
|
|
/// recover_count_required;不动 params_json / expression / rule_type / scene_code / data_source_id。
|
|
|
/// </summary>
|
|
|
- public async Task<AdoS8WatchRule> UpdateScheduleAsync(long id, S8WatchRuleSchedulePayload payload, S8TrustedScope scope)
|
|
|
- {
|
|
|
- var entity = await LoadScopedAsync(id, scope);
|
|
|
-
|
|
|
- if (payload.PollIntervalSeconds < 60 || payload.PollIntervalSeconds > 86400)
|
|
|
- throw new S8BizException("poll_interval_seconds 必须在 60–86400 之间");
|
|
|
- if (payload.TriggerCountRequired < 1 || payload.TriggerCountRequired > 10)
|
|
|
- throw new S8BizException("trigger_count_required 必须在 1–10 之间");
|
|
|
- if (payload.RecoverCountRequired < 1 || payload.RecoverCountRequired > 10)
|
|
|
- throw new S8BizException("recover_count_required 必须在 1–10 之间");
|
|
|
-
|
|
|
- await _rep.Context.Updateable<AdoS8WatchRule>()
|
|
|
- .SetColumns(x => new AdoS8WatchRule
|
|
|
- {
|
|
|
- PollIntervalSeconds = payload.PollIntervalSeconds,
|
|
|
- TriggerCountRequired = payload.TriggerCountRequired,
|
|
|
- RecoverCountRequired = payload.RecoverCountRequired,
|
|
|
- UpdatedAt = DateTime.Now
|
|
|
- })
|
|
|
- .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
|
|
|
- .ExecuteCommandAsync();
|
|
|
- return await LoadScopedAsync(id, scope);
|
|
|
- }
|
|
|
+ public Task<AdoS8WatchRule> UpdateScheduleAsync(long id, S8WatchRuleSchedulePayload payload, S8TrustedScope scope) =>
|
|
|
+ // S8-RULE-GOVERNANCE-BATCH1:三个调度字段已并入统一参数白名单,本入口只做形状转换后委托,
|
|
|
+ // 不再各自维护一份取值域 —— 两处取值域一旦漂移,就会出现「A 接口存得下、B 接口存不下」。
|
|
|
+ UpdateParametersAsync(id, new S8RuleParametersPayload
|
|
|
+ {
|
|
|
+ PollIntervalSeconds = payload?.PollIntervalSeconds,
|
|
|
+ TriggerCountRequired = payload?.TriggerCountRequired,
|
|
|
+ RecoverCountRequired = payload?.RecoverCountRequired
|
|
|
+ }, scope);
|
|
|
|
|
|
/// <summary>
|
|
|
/// S8-SCHED-FRONTEND-1:立即执行一次。把 next_run_at 置为 NOW,让下个 tick 拾取。
|
|
|
@@ -355,8 +452,13 @@ public class S8WatchRuleService : ITransient
|
|
|
S8WatchRuleDataAccessValidator.ValidateForSave(body, _datasetCatalog);
|
|
|
|
|
|
// 只有真正要启用时才要求完整运行条件;草稿 / disabled 规则允许 Provider 未上线。
|
|
|
+ // S8-RULE-GOVERNANCE-BATCH1:启用条件按**代码定义**判定,不按 body 上的投影列。
|
|
|
if (body.Enabled)
|
|
|
- S8WatchRuleDataAccessValidator.ValidateForEnable(body, _datasetEnableGate, scope.TenantId, scope.FactoryId);
|
|
|
+ {
|
|
|
+ var definition = _ruleCatalog.GetRequired(body.RuleCode);
|
|
|
+ _datasetEnableGate.EnsureCanEnable(
|
|
|
+ definition.DatasetCode, definition.RuleType, definition.RuleCode, scope.TenantId, scope.FactoryId);
|
|
|
+ }
|
|
|
|
|
|
var scene = await _sceneRep.GetFirstAsync(x => x.TenantId == body.TenantId && x.FactoryId == body.FactoryId && x.SceneCode == body.SceneCode)
|
|
|
?? throw new S8BizException("关联场景不存在");
|