|
|
@@ -1,3 +1,6 @@
|
|
|
+using System.Globalization;
|
|
|
+using System.Text.Json;
|
|
|
+using System.Text.Json.Serialization;
|
|
|
using Admin.NET.Plugin.AiDOP.Dto.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
|
|
|
@@ -13,15 +16,46 @@ public class S8ConfigDraftService : ITransient
|
|
|
{
|
|
|
private readonly SqlSugarRepository<AdoS8ConfigDraft> _rep;
|
|
|
private readonly S8WatchRuleService _watchRuleService;
|
|
|
+ // CONFIG-WIZARD-GENERATE-RULE-SERVER-BUILD-1:rulePayload 缺省时按 wizard_json 重建用到的字典 Repository。
|
|
|
+ private readonly SqlSugarRepository<AdoS8MonitorObject> _monitorObjectRep;
|
|
|
+ private readonly SqlSugarRepository<AdoS8MonitorMetric> _monitorMetricRep;
|
|
|
+ private readonly SqlSugarRepository<AdoS8ExceptionType> _exceptionTypeRep;
|
|
|
+ private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
|
|
|
+ private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
|
|
|
|
|
|
public S8ConfigDraftService(
|
|
|
SqlSugarRepository<AdoS8ConfigDraft> rep,
|
|
|
- S8WatchRuleService watchRuleService)
|
|
|
+ S8WatchRuleService watchRuleService,
|
|
|
+ SqlSugarRepository<AdoS8MonitorObject> monitorObjectRep,
|
|
|
+ SqlSugarRepository<AdoS8MonitorMetric> monitorMetricRep,
|
|
|
+ SqlSugarRepository<AdoS8ExceptionType> exceptionTypeRep,
|
|
|
+ SqlSugarRepository<AdoS8DataSource> dataSourceRep,
|
|
|
+ SqlSugarRepository<AdoS8SceneConfig> sceneRep)
|
|
|
{
|
|
|
_rep = rep;
|
|
|
_watchRuleService = watchRuleService;
|
|
|
+ _monitorObjectRep = monitorObjectRep;
|
|
|
+ _monitorMetricRep = monitorMetricRep;
|
|
|
+ _exceptionTypeRep = exceptionTypeRep;
|
|
|
+ _dataSourceRep = dataSourceRep;
|
|
|
+ _sceneRep = sceneRep;
|
|
|
}
|
|
|
|
|
|
+ // CONFIG-WIZARD-GENERATE-RULE-SERVER-BUILD-1:解析 wizard_json 用 JsonSerializerOptions(前端 camelCase)。
|
|
|
+ private static readonly JsonSerializerOptions WizardJsonReadOptions = new()
|
|
|
+ {
|
|
|
+ PropertyNameCaseInsensitive = true,
|
|
|
+ ReadCommentHandling = JsonCommentHandling.Skip,
|
|
|
+ AllowTrailingCommas = true,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 后端生成 params_json 时 camelCase + 忽略 null。
|
|
|
+ private static readonly JsonSerializerOptions ParamsJsonWriteOptions = new()
|
|
|
+ {
|
|
|
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
|
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
|
+ };
|
|
|
+
|
|
|
public async Task<AdoS8ConfigDraftListResultDto> ListAsync(AdoS8ConfigDraftListQueryDto query)
|
|
|
{
|
|
|
var page = query.Page < 1 ? 1 : query.Page;
|
|
|
@@ -150,9 +184,10 @@ public class S8ConfigDraftService : ITransient
|
|
|
if (draft.Status == S8ConfigDraftStatus.Generated)
|
|
|
throw new S8BizException("草稿状态异常:已标记 GENERATED 但未回写规则 ID");
|
|
|
|
|
|
- var payload = dto.RulePayload ?? throw new S8BizException("rulePayload 必填");
|
|
|
+ // CONFIG-WIZARD-GENERATE-RULE-SERVER-BUILD-1:兼容模式 — RulePayload 缺省时按 wizard_json 重建。
|
|
|
+ var payload = dto.RulePayload ?? await RebuildPayloadFromWizardJsonAsync(draft);
|
|
|
if (string.IsNullOrWhiteSpace(payload.RuleCode))
|
|
|
- throw new S8BizException("rulePayload.ruleCode 必填");
|
|
|
+ throw new S8BizException("规则编码不能为空");
|
|
|
|
|
|
// 安全收口:强制对齐草稿租户/工厂、强制 enabled=false。
|
|
|
payload.Id = 0;
|
|
|
@@ -241,4 +276,324 @@ public class S8ConfigDraftService : ITransient
|
|
|
var trimmed = value.Trim();
|
|
|
return trimmed.Length == 0 ? null : trimmed;
|
|
|
}
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // CONFIG-WIZARD-GENERATE-RULE-SERVER-BUILD-1:基于 wizard_json + 字典重建 AdoS8WatchRule
|
|
|
+ // ============================================================
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 从草稿的 wizard_json 解析向导态,结合 monitor object/metric、exception_type、data_source、scene_config
|
|
|
+ /// 重建 AdoS8WatchRule。所有错误统一抛 S8BizException → Controller 转 400。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<AdoS8WatchRule> RebuildPayloadFromWizardJsonAsync(AdoS8ConfigDraft draft)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(draft.WizardJson))
|
|
|
+ throw new S8BizException("草稿配置内容为空");
|
|
|
+
|
|
|
+ S8WizardJsonV1 parsed;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ parsed = JsonSerializer.Deserialize<S8WizardJsonV1>(draft.WizardJson, WizardJsonReadOptions)
|
|
|
+ ?? throw new S8BizException("草稿配置内容为空");
|
|
|
+ }
|
|
|
+ catch (JsonException)
|
|
|
+ {
|
|
|
+ throw new S8BizException("草稿配置内容格式错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (parsed.Version != 1)
|
|
|
+ throw new S8BizException($"暂不支持该草稿版本(version={parsed.Version})");
|
|
|
+
|
|
|
+ var form = parsed.Form ?? throw new S8BizException("草稿配置内容不完整");
|
|
|
+ var labels = parsed.Labels;
|
|
|
+
|
|
|
+ // 必填基础字段
|
|
|
+ var ruleCode = NormalizeOrNull(form.RuleCode) ?? throw new S8BizException("规则编码不能为空");
|
|
|
+ var mechanism = NormalizeOrNull(form.Mechanism) ?? throw new S8BizException("报警机制未选择");
|
|
|
+ if (mechanism == "MANUAL_REPORT")
|
|
|
+ throw new S8BizException("主动提报无需生成自动监控规则");
|
|
|
+ var stageCode = NormalizeOrNull(form.StageCode) ?? throw new S8BizException("阶段维度未选择");
|
|
|
+ var exceptionTypeCode = NormalizeOrNull(form.ExceptionTypeCode) ?? throw new S8BizException("异常类型未选择");
|
|
|
+ var objectType = NormalizeOrNull(form.ObjectType) ?? throw new S8BizException("监控对象未选择");
|
|
|
+ var objectLabel = NormalizeOrNull(form.ObjectLabel) ?? throw new S8BizException("监控对象未选择");
|
|
|
+ var metricCode = NormalizeOrNull(form.MetricCode) ?? throw new S8BizException("监控指标未选择");
|
|
|
+
|
|
|
+ // 监控对象(用 objectType + objectName 在字典里反查;tenant 覆盖 baseline)
|
|
|
+ var monitorObject = await ResolveMonitorObjectAsync(draft.TenantId, draft.FactoryId, objectType, objectLabel);
|
|
|
+
|
|
|
+ // 监控指标
|
|
|
+ var metric = await ResolveMonitorMetricAsync(draft.TenantId, draft.FactoryId, metricCode, mechanism);
|
|
|
+
|
|
|
+ // 异常类型(baseline 0/0 + tenant 覆盖;enabled 必须 true;sceneCode 必须等于 stageCode)
|
|
|
+ var exceptionType = await ResolveExceptionTypeAsync(draft.TenantId, draft.FactoryId, exceptionTypeCode);
|
|
|
+ if (!string.IsNullOrWhiteSpace(exceptionType.SceneCode) && exceptionType.SceneCode != stageCode)
|
|
|
+ throw new S8BizException("异常类型所属阶段与当前规则阶段不一致");
|
|
|
+
|
|
|
+ // severity 优先 wizard_json,再 exceptionType.SeverityDefault,再 FOLLOW
|
|
|
+ var severity = NormalizeOrNull(form.Severity)
|
|
|
+ ?? NormalizeOrNull(exceptionType.SeverityDefault)
|
|
|
+ ?? "FOLLOW";
|
|
|
+
|
|
|
+ // dataSource:tenant/factory 下第一个 enabled
|
|
|
+ var dataSource = await _dataSourceRep.AsQueryable()
|
|
|
+ .Where(x => x.TenantId == draft.TenantId && x.FactoryId == draft.FactoryId && x.Enabled)
|
|
|
+ .OrderBy(x => x.Id)
|
|
|
+ .FirstAsync()
|
|
|
+ ?? throw new S8BizException("未找到可用数据源");
|
|
|
+
|
|
|
+ // scene_config:tenant/factory + stageCode + enabled(CreateAsync 也会校验,提前查给更清晰错误)
|
|
|
+ var scene = await _sceneRep.AsQueryable()
|
|
|
+ .FirstAsync(x => x.TenantId == draft.TenantId && x.FactoryId == draft.FactoryId && x.SceneCode == stageCode)
|
|
|
+ ?? throw new S8BizException("规则所属场景不存在");
|
|
|
+ if (!scene.Enabled) throw new S8BizException("规则所属场景未启用");
|
|
|
+
|
|
|
+ // params_json + expression
|
|
|
+ var paramsJson = BuildParamsJson(form, labels, mechanism, metric, monitorObject, exceptionTypeCode);
|
|
|
+ var expression = BuildExpression(mechanism);
|
|
|
+
|
|
|
+ return new AdoS8WatchRule
|
|
|
+ {
|
|
|
+ TenantId = draft.TenantId,
|
|
|
+ FactoryId = draft.FactoryId,
|
|
|
+ RuleCode = ruleCode,
|
|
|
+ SceneCode = stageCode,
|
|
|
+ DataSourceId = dataSource.Id,
|
|
|
+ WatchObjectType = monitorObject.ObjectType,
|
|
|
+ RuleType = RuleTypeOf(mechanism),
|
|
|
+ RuleMechanism = mechanism,
|
|
|
+ StageCode = stageCode,
|
|
|
+ OrderFlowCode = NormalizeOrNull(form.OrderFlowCode),
|
|
|
+ SourceObjectType = monitorObject.ObjectType,
|
|
|
+ Severity = severity,
|
|
|
+ Expression = expression,
|
|
|
+ ParamsJson = paramsJson,
|
|
|
+ PollIntervalSeconds = form.PollIntervalSeconds is > 0 ? form.PollIntervalSeconds.Value : 300,
|
|
|
+ TriggerCountRequired = form.TriggerCountRequired is > 0 ? form.TriggerCountRequired.Value : 1,
|
|
|
+ RecoverCountRequired = form.RecoverCountRequired is > 0 ? form.RecoverCountRequired.Value : 1,
|
|
|
+ ConsecutiveFailureCount = 0,
|
|
|
+ Enabled = false,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<AdoS8MonitorObject> ResolveMonitorObjectAsync(long tenantId, long factoryId, string objectType, string objectName)
|
|
|
+ {
|
|
|
+ var rows = await _monitorObjectRep.AsQueryable()
|
|
|
+ .Where(x => x.ObjectType == objectType && x.ObjectName == objectName
|
|
|
+ && ((x.TenantId == 0 && x.FactoryId == 0)
|
|
|
+ || (x.TenantId == tenantId && x.FactoryId == factoryId)))
|
|
|
+ .ToListAsync();
|
|
|
+ var picked = rows
|
|
|
+ .OrderByDescending(x => x.FactoryId)
|
|
|
+ .ThenByDescending(x => x.TenantId)
|
|
|
+ .FirstOrDefault();
|
|
|
+ if (picked == null || !picked.Enabled)
|
|
|
+ throw new S8BizException($"监控对象 '{objectName}' 不存在或未启用");
|
|
|
+ return picked;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<AdoS8MonitorMetric> ResolveMonitorMetricAsync(long tenantId, long factoryId, string metricCode, string mechanism)
|
|
|
+ {
|
|
|
+ var rows = await _monitorMetricRep.AsQueryable()
|
|
|
+ .Where(x => x.MetricCode == metricCode
|
|
|
+ && ((x.TenantId == 0 && x.FactoryId == 0)
|
|
|
+ || (x.TenantId == tenantId && x.FactoryId == factoryId)))
|
|
|
+ .ToListAsync();
|
|
|
+ var picked = rows
|
|
|
+ .OrderByDescending(x => x.FactoryId)
|
|
|
+ .ThenByDescending(x => x.TenantId)
|
|
|
+ .FirstOrDefault();
|
|
|
+ if (picked == null)
|
|
|
+ throw new S8BizException($"监控指标 '{metricCode}' 不存在");
|
|
|
+ if (!picked.Enabled)
|
|
|
+ {
|
|
|
+ var label = string.IsNullOrWhiteSpace(picked.MetricName) ? picked.MetricCode : picked.MetricName;
|
|
|
+ throw new S8BizException($"监控指标 '{label}' 未启用,请先在监控指标字典中启用后再生成规则");
|
|
|
+ }
|
|
|
+ if (!string.IsNullOrWhiteSpace(picked.Mechanism) && picked.Mechanism != mechanism)
|
|
|
+ throw new S8BizException("监控指标与报警机制不匹配");
|
|
|
+ return picked;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<AdoS8ExceptionType> ResolveExceptionTypeAsync(long tenantId, long factoryId, string typeCode)
|
|
|
+ {
|
|
|
+ var rows = await _exceptionTypeRep.AsQueryable()
|
|
|
+ .Where(x => x.TypeCode == typeCode
|
|
|
+ && ((x.TenantId == 0 && x.FactoryId == 0)
|
|
|
+ || (x.TenantId == tenantId && x.FactoryId == factoryId)))
|
|
|
+ .ToListAsync();
|
|
|
+ var picked = rows.OrderByDescending(x => x.FactoryId).ThenByDescending(x => x.TenantId).FirstOrDefault();
|
|
|
+ if (picked == null) throw new S8BizException($"异常类型 '{typeCode}' 不存在");
|
|
|
+ if (!picked.Enabled) throw new S8BizException($"异常类型 '{picked.TypeName}' 未启用");
|
|
|
+ return picked;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string RuleTypeOf(string mechanism) => mechanism switch
|
|
|
+ {
|
|
|
+ "DATE" => "TIMEOUT",
|
|
|
+ "VALUE_RANGE" => "OUT_OF_RANGE",
|
|
|
+ "RATIO" => "OUT_OF_RANGE",
|
|
|
+ "MANUAL_REPORT" => throw new S8BizException("主动提报无需生成自动监控规则"),
|
|
|
+ _ => throw new S8BizException($"不支持的报警机制:{mechanism}"),
|
|
|
+ };
|
|
|
+
|
|
|
+ private static string BuildExpression(string mechanism) =>
|
|
|
+ mechanism == "DATE"
|
|
|
+ ? "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, DATE_SUB(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status WHERE 1=0"
|
|
|
+ : "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, 0 AS measured_value WHERE 1=0";
|
|
|
+
|
|
|
+ private static string BuildParamsJson(
|
|
|
+ S8WizardFormV1 form,
|
|
|
+ S8WizardLabelsV1? labels,
|
|
|
+ string mechanism,
|
|
|
+ AdoS8MonitorMetric metric,
|
|
|
+ AdoS8MonitorObject monitorObject,
|
|
|
+ string exceptionTypeCode)
|
|
|
+ {
|
|
|
+ var objectLabel = NormalizeOrNull(labels?.ObjectLabel)
|
|
|
+ ?? NormalizeOrNull(form.ObjectLabel)
|
|
|
+ ?? monitorObject.ObjectName;
|
|
|
+ var metricLabel = NormalizeOrNull(labels?.MetricLabel)
|
|
|
+ ?? NormalizeOrNull(form.MetricLabel)
|
|
|
+ ?? metric.MetricName;
|
|
|
+ var unit = NormalizeOrNull(form.Unit) ?? metric.Unit;
|
|
|
+ var thresholdDisplay = NormalizeOrNull(labels?.ThresholdDisplay)
|
|
|
+ ?? BuildThresholdDisplay(mechanism, form, metric, unit);
|
|
|
+
|
|
|
+ var objectIdField = NormalizeOrNull(metric.ObjectIdField) ?? "source_object_id";
|
|
|
+ var objectCodeField = NormalizeOrNull(metric.ObjectCodeField) ?? "related_object_code";
|
|
|
+ var objectNameField = NormalizeOrNull(metric.ObjectNameField) ?? "related_object_name";
|
|
|
+
|
|
|
+ var dict = new Dictionary<string, object?>
|
|
|
+ {
|
|
|
+ ["objectIdField"] = objectIdField,
|
|
|
+ ["objectCodeField"] = objectCodeField,
|
|
|
+ ["objectNameField"] = objectNameField,
|
|
|
+ ["exceptionTypeCode"] = exceptionTypeCode,
|
|
|
+ ["objectLabel"] = objectLabel,
|
|
|
+ ["metricLabel"] = metricLabel,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (mechanism == "DATE")
|
|
|
+ {
|
|
|
+ var states = ParseCsvStates(form.CompletedStates);
|
|
|
+ if (states.Count == 0) states = ParseCsvStates(metric.DefaultCompletedStates);
|
|
|
+ if (states.Count == 0) states = new List<string> { "CLOSED", "COMPLETED", "DONE" };
|
|
|
+
|
|
|
+ var grace = form.GraceMinutes ?? metric.DefaultGraceMinutes ?? 0;
|
|
|
+ dict["dueAtField"] = NormalizeOrNull(metric.DueAtField) ?? "due_at";
|
|
|
+ dict["statusField"] = NormalizeOrNull(metric.StatusField) ?? "status";
|
|
|
+ dict["completedStates"] = states;
|
|
|
+ dict["graceMinutes"] = grace;
|
|
|
+ dict["unit"] = unit ?? "分钟";
|
|
|
+ dict["thresholdDisplay"] = thresholdDisplay;
|
|
|
+ }
|
|
|
+ else if (mechanism == "VALUE_RANGE")
|
|
|
+ {
|
|
|
+ var lower = form.LowerBound ?? metric.DefaultLowerBound;
|
|
|
+ var upper = form.UpperBound ?? metric.DefaultUpperBound;
|
|
|
+ if (lower == null && upper == null)
|
|
|
+ throw new S8BizException("请配置上限或下限至少其一");
|
|
|
+ dict["measuredValueField"] = NormalizeOrNull(metric.MeasuredValueField) ?? "measured_value";
|
|
|
+ dict["unit"] = unit;
|
|
|
+ dict["thresholdDisplay"] = thresholdDisplay;
|
|
|
+ if (lower.HasValue) dict["lowerBound"] = lower.Value;
|
|
|
+ if (upper.HasValue) dict["upperBound"] = upper.Value;
|
|
|
+ if (form.ToleranceAbs is > 0) dict["toleranceAbs"] = form.ToleranceAbs.Value;
|
|
|
+ if (form.ToleranceRatioPct is > 0) dict["toleranceRatio"] = form.ToleranceRatioPct.Value / 100m;
|
|
|
+ }
|
|
|
+ else if (mechanism == "RATIO")
|
|
|
+ {
|
|
|
+ var target = form.TargetRatio ?? metric.DefaultTargetRatio;
|
|
|
+ if (!target.HasValue) throw new S8BizException("请配置目标比例");
|
|
|
+ dict["measuredValueField"] = NormalizeOrNull(metric.MeasuredValueField) ?? "measured_value";
|
|
|
+ dict["lowerBound"] = target.Value;
|
|
|
+ dict["unit"] = "%";
|
|
|
+ dict["thresholdDisplay"] = thresholdDisplay;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ throw new S8BizException($"不支持的报警机制:{mechanism}");
|
|
|
+ }
|
|
|
+
|
|
|
+ return JsonSerializer.Serialize(dict, ParamsJsonWriteOptions);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<string> ParseCsvStates(string? csv)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(csv)) return new List<string>();
|
|
|
+ return csv.Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
|
+ .ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string BuildThresholdDisplay(string mechanism, S8WizardFormV1 form, AdoS8MonitorMetric metric, string? unit)
|
|
|
+ {
|
|
|
+ var u = unit ?? string.Empty;
|
|
|
+ if (mechanism == "DATE")
|
|
|
+ {
|
|
|
+ var grace = form.GraceMinutes ?? metric.DefaultGraceMinutes ?? 0;
|
|
|
+ return grace > 0 ? $"超期 {grace} 分钟仍未完成触发" : "超期即触发";
|
|
|
+ }
|
|
|
+ if (mechanism == "RATIO")
|
|
|
+ {
|
|
|
+ var target = form.TargetRatio ?? metric.DefaultTargetRatio;
|
|
|
+ return target.HasValue
|
|
|
+ ? string.Format(CultureInfo.InvariantCulture, "低于 {0}% 触发", target.Value)
|
|
|
+ : "低于目标比例触发";
|
|
|
+ }
|
|
|
+ if (mechanism == "VALUE_RANGE")
|
|
|
+ {
|
|
|
+ var lower = form.LowerBound ?? metric.DefaultLowerBound;
|
|
|
+ var upper = form.UpperBound ?? metric.DefaultUpperBound;
|
|
|
+ var segs = new List<string>();
|
|
|
+ if (lower.HasValue) segs.Add(string.Format(CultureInfo.InvariantCulture, "低于 {0}{1}", lower.Value, u));
|
|
|
+ if (upper.HasValue) segs.Add(string.Format(CultureInfo.InvariantCulture, "高于 {0}{1}", upper.Value, u));
|
|
|
+ return segs.Count > 0 ? string.Join(" 或 ", segs) + " 触发" : "超出设定范围触发";
|
|
|
+ }
|
|
|
+ return string.Empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- wizard_json 解析私有 DTO -----
|
|
|
+
|
|
|
+ private sealed class S8WizardJsonV1
|
|
|
+ {
|
|
|
+ public int Version { get; set; }
|
|
|
+ public int Step { get; set; }
|
|
|
+ public S8WizardFormV1? Form { get; set; }
|
|
|
+ public S8WizardLabelsV1? Labels { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class S8WizardFormV1
|
|
|
+ {
|
|
|
+ public string? Mechanism { get; set; }
|
|
|
+ public string? StageCode { get; set; }
|
|
|
+ public string? OrderFlowCode { get; set; }
|
|
|
+ public string? ExceptionTypeCode { get; set; }
|
|
|
+ public string? Severity { get; set; }
|
|
|
+ public string? ObjectType { get; set; }
|
|
|
+ public string? ObjectLabel { get; set; }
|
|
|
+ public string? MetricCode { get; set; }
|
|
|
+ public string? MetricLabel { get; set; }
|
|
|
+ public string? Unit { get; set; }
|
|
|
+ public int? GraceMinutes { get; set; }
|
|
|
+ // 前端 wizard_json 中 completedStates 是 CSV 字符串(如 "CLOSED,COMPLETED,DONE"),不是数组
|
|
|
+ public string? CompletedStates { get; set; }
|
|
|
+ public decimal? LowerBound { get; set; }
|
|
|
+ public decimal? UpperBound { get; set; }
|
|
|
+ public decimal? ToleranceAbs { get; set; }
|
|
|
+ public decimal? ToleranceRatioPct { get; set; }
|
|
|
+ public decimal? TargetRatio { get; set; }
|
|
|
+ public string? RuleCode { get; set; }
|
|
|
+ public int? PollIntervalSeconds { get; set; }
|
|
|
+ public int? TriggerCountRequired { get; set; }
|
|
|
+ public int? RecoverCountRequired { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class S8WizardLabelsV1
|
|
|
+ {
|
|
|
+ public string? MechanismLabel { get; set; }
|
|
|
+ public string? StageLabel { get; set; }
|
|
|
+ public string? ObjectLabel { get; set; }
|
|
|
+ public string? MetricLabel { get; set; }
|
|
|
+ public string? ThresholdDisplay { get; set; }
|
|
|
+ }
|
|
|
}
|