|
|
@@ -1,631 +0,0 @@
|
|
|
-using System.Globalization;
|
|
|
-using System.Text.Json;
|
|
|
-using System.Text.Json.Serialization;
|
|
|
-using System.Text.RegularExpressions;
|
|
|
-using Admin.NET.Plugin.AiDOP.Dto.S8;
|
|
|
-using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
-using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
-
|
|
|
-using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
|
|
|
-
|
|
|
-namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
-
|
|
|
-internal static class S8ConfigDraftStatus
|
|
|
-{
|
|
|
- public const string InProgress = "IN_PROGRESS";
|
|
|
- public const string Generated = "GENERATED";
|
|
|
-}
|
|
|
-
|
|
|
-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 IS8DatasetCatalog _datasetCatalog;
|
|
|
- private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
|
|
|
-
|
|
|
- public S8ConfigDraftService(
|
|
|
- SqlSugarRepository<AdoS8ConfigDraft> rep,
|
|
|
- S8WatchRuleService watchRuleService,
|
|
|
- SqlSugarRepository<AdoS8MonitorObject> monitorObjectRep,
|
|
|
- SqlSugarRepository<AdoS8MonitorMetric> monitorMetricRep,
|
|
|
- SqlSugarRepository<AdoS8ExceptionType> exceptionTypeRep,
|
|
|
- IS8DatasetCatalog datasetCatalog,
|
|
|
- SqlSugarRepository<AdoS8SceneConfig> sceneRep)
|
|
|
- {
|
|
|
- _rep = rep;
|
|
|
- _watchRuleService = watchRuleService;
|
|
|
- _monitorObjectRep = monitorObjectRep;
|
|
|
- _monitorMetricRep = monitorMetricRep;
|
|
|
- _exceptionTypeRep = exceptionTypeRep;
|
|
|
- _datasetCatalog = datasetCatalog;
|
|
|
- _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;
|
|
|
- var pageSize = query.PageSize < 1 ? 20 : Math.Min(query.PageSize, 200);
|
|
|
- var status = string.IsNullOrWhiteSpace(query.Status) ? null : query.Status.Trim();
|
|
|
-
|
|
|
- var q = _rep.AsQueryable()
|
|
|
- .Where(x => x.TenantId == query.TenantId && x.FactoryId == query.FactoryId);
|
|
|
- if (status != null)
|
|
|
- q = q.Where(x => x.Status == status);
|
|
|
-
|
|
|
- RefAsync<int> total = 0;
|
|
|
- var rows = await q
|
|
|
- .OrderBy(x => x.Id, OrderByType.Desc)
|
|
|
- .Select(x => new AdoS8ConfigDraftListItemDto
|
|
|
- {
|
|
|
- Id = x.Id,
|
|
|
- TenantId = x.TenantId,
|
|
|
- FactoryId = x.FactoryId,
|
|
|
- DraftCode = x.DraftCode,
|
|
|
- DraftName = x.DraftName,
|
|
|
- Status = x.Status,
|
|
|
- CurrentStep = x.CurrentStep,
|
|
|
- Mechanism = x.Mechanism,
|
|
|
- StageCode = x.StageCode,
|
|
|
- OrderFlowCode = x.OrderFlowCode,
|
|
|
- ExceptionTypeCode = x.ExceptionTypeCode,
|
|
|
- GeneratedRuleId = x.GeneratedRuleId,
|
|
|
- Remark = x.Remark,
|
|
|
- CreatedAt = x.CreatedAt,
|
|
|
- UpdatedAt = x.UpdatedAt
|
|
|
- })
|
|
|
- .ToPageListAsync(page, pageSize, total);
|
|
|
-
|
|
|
- return new AdoS8ConfigDraftListResultDto
|
|
|
- {
|
|
|
- Items = rows,
|
|
|
- Total = total.Value,
|
|
|
- Page = page,
|
|
|
- PageSize = pageSize
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // S8-TENANT-FACTORY-P0-CLOSURE-1:按 Id + 可信作用域绑行;越权 Id 视为不存在。
|
|
|
- public async Task<AdoS8ConfigDraftDetailDto> GetAsync(long id, S8TrustedScope scope)
|
|
|
- {
|
|
|
- var e = await LoadScopedAsync(id, scope);
|
|
|
- return ToDetail(e);
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>按 Id + 可信作用域取草稿;不在作用域内一律按「不存在」处理。</summary>
|
|
|
- private async Task<AdoS8ConfigDraft> LoadScopedAsync(long id, S8TrustedScope scope) =>
|
|
|
- await _rep.AsQueryable()
|
|
|
- .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
|
|
|
- .FirstAsync() ?? throw new S8NotFoundException("草稿不存在");
|
|
|
-
|
|
|
- // S8-TENANT-FACTORY-P0-CLOSURE-1:归属一律由服务端可信作用域盖章,忽略 dto.TenantId / dto.FactoryId。
|
|
|
- public async Task<AdoS8ConfigDraftDetailDto> CreateAsync(AdoS8ConfigDraftCreateDto dto, S8TrustedScope scope)
|
|
|
- {
|
|
|
- if (string.IsNullOrWhiteSpace(dto.DraftCode))
|
|
|
- throw new S8BizException("草稿编码必填");
|
|
|
- if (string.IsNullOrWhiteSpace(dto.WizardJson))
|
|
|
- throw new S8BizException("草稿 wizard_json 必填");
|
|
|
-
|
|
|
- var draftCode = dto.DraftCode.Trim();
|
|
|
- var exists = await _rep.AsQueryable()
|
|
|
- .AnyAsync(x => x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId && x.DraftCode == draftCode);
|
|
|
- if (exists) throw new S8BizException("草稿编码已存在");
|
|
|
-
|
|
|
- var entity = new AdoS8ConfigDraft
|
|
|
- {
|
|
|
- TenantId = scope.TenantId,
|
|
|
- FactoryId = scope.FactoryId,
|
|
|
- DraftCode = draftCode,
|
|
|
- DraftName = NormalizeOrNull(dto.DraftName),
|
|
|
- WizardJson = dto.WizardJson,
|
|
|
- CurrentStep = dto.CurrentStep ?? 0,
|
|
|
- Mechanism = NormalizeOrNull(dto.Mechanism),
|
|
|
- StageCode = NormalizeOrNull(dto.StageCode),
|
|
|
- OrderFlowCode = NormalizeOrNull(dto.OrderFlowCode),
|
|
|
- ExceptionTypeCode = NormalizeOrNull(dto.ExceptionTypeCode),
|
|
|
- Remark = NormalizeOrNull(dto.Remark),
|
|
|
- Status = S8ConfigDraftStatus.InProgress,
|
|
|
- CreatedAt = DateTime.Now
|
|
|
- };
|
|
|
- entity.Id = await _rep.AsInsertable(entity).ExecuteReturnBigIdentityAsync();
|
|
|
- return ToDetail(entity);
|
|
|
- }
|
|
|
-
|
|
|
- public async Task<AdoS8ConfigDraftDetailDto> UpdateAsync(long id, AdoS8ConfigDraftUpdateDto dto, S8TrustedScope scope)
|
|
|
- {
|
|
|
- var e = await LoadScopedAsync(id, scope);
|
|
|
- if (e.Status == S8ConfigDraftStatus.Generated)
|
|
|
- throw new S8BizException("草稿已生成正式规则,禁止再编辑");
|
|
|
-
|
|
|
- if (dto.DraftName != null) e.DraftName = NormalizeOrNull(dto.DraftName);
|
|
|
- if (dto.WizardJson != null)
|
|
|
- {
|
|
|
- if (string.IsNullOrWhiteSpace(dto.WizardJson))
|
|
|
- throw new S8BizException("wizard_json 不能为空字符串");
|
|
|
- e.WizardJson = dto.WizardJson;
|
|
|
- }
|
|
|
- if (dto.CurrentStep.HasValue) e.CurrentStep = dto.CurrentStep.Value;
|
|
|
- if (dto.Mechanism != null) e.Mechanism = NormalizeOrNull(dto.Mechanism);
|
|
|
- if (dto.StageCode != null) e.StageCode = NormalizeOrNull(dto.StageCode);
|
|
|
- if (dto.OrderFlowCode != null) e.OrderFlowCode = NormalizeOrNull(dto.OrderFlowCode);
|
|
|
- if (dto.ExceptionTypeCode != null) e.ExceptionTypeCode = NormalizeOrNull(dto.ExceptionTypeCode);
|
|
|
- if (dto.Remark != null) e.Remark = NormalizeOrNull(dto.Remark);
|
|
|
-
|
|
|
- e.UpdatedAt = DateTime.Now;
|
|
|
- await _rep.UpdateAsync(e);
|
|
|
- return ToDetail(e);
|
|
|
- }
|
|
|
-
|
|
|
- public async Task DeleteAsync(long id, S8TrustedScope scope)
|
|
|
- {
|
|
|
- var e = await LoadScopedAsync(id, scope);
|
|
|
- await _rep.DeleteByIdAsync(e.Id);
|
|
|
- }
|
|
|
-
|
|
|
- public async Task<AdoS8ConfigDraftGenerateRuleResultDto> GenerateRuleAsync(long id, AdoS8ConfigDraftGenerateRuleDto dto, S8TrustedScope scope)
|
|
|
- {
|
|
|
- var draft = await LoadScopedAsync(id, scope);
|
|
|
-
|
|
|
- if (draft.GeneratedRuleId.HasValue)
|
|
|
- {
|
|
|
- return new AdoS8ConfigDraftGenerateRuleResultDto
|
|
|
- {
|
|
|
- RuleId = draft.GeneratedRuleId.Value,
|
|
|
- RuleCode = await ResolveExistingRuleCodeAsync(draft.GeneratedRuleId.Value),
|
|
|
- DraftStatus = draft.Status
|
|
|
- };
|
|
|
- }
|
|
|
- if (draft.Status == S8ConfigDraftStatus.Generated)
|
|
|
- throw new S8BizException("草稿状态异常:已标记 GENERATED 但未回写规则 ID");
|
|
|
-
|
|
|
- // CONFIG-WIZARD-API-RULEPAYLOAD-CLEANUP-1:始终由后端基于 wizard_json + 字典重建,dto 已不再承载 RulePayload。
|
|
|
- var payload = await RebuildPayloadFromWizardJsonAsync(draft);
|
|
|
- if (string.IsNullOrWhiteSpace(payload.RuleCode))
|
|
|
- throw new S8BizException("规则编码不能为空");
|
|
|
-
|
|
|
- // 安全收口:强制对齐草稿租户/工厂、强制 enabled=false。
|
|
|
- payload.Id = 0;
|
|
|
- payload.TenantId = draft.TenantId;
|
|
|
- payload.FactoryId = draft.FactoryId;
|
|
|
- payload.Enabled = false;
|
|
|
-
|
|
|
- var db = _rep.Context;
|
|
|
- await db.Ado.BeginTranAsync();
|
|
|
- try
|
|
|
- {
|
|
|
- // S8-TENANT-FACTORY-P0-CLOSURE-1:草稿已按可信作用域绑行,生成的规则沿用同一作用域。
|
|
|
- //
|
|
|
- // S8-STANDARD-DATASET-HARD-CUTOVER-1:向导不再生成 SQL,只组装 dataset_code + params_json。
|
|
|
- // 因此也不再需要 origin 形参去区分"谁写的 SQL"——没有 SQL 了。
|
|
|
- var created = await _watchRuleService.CreateAsync(payload, scope);
|
|
|
-
|
|
|
- // S8WatchRuleService.CreateAsync 内部用 InsertAsync 不回填 Id;按 RuleCode 反查真实 id。
|
|
|
- var newRuleId = created.Id;
|
|
|
- if (newRuleId <= 0)
|
|
|
- {
|
|
|
- newRuleId = await db.Queryable<AdoS8WatchRule>()
|
|
|
- .Where(x => x.TenantId == draft.TenantId
|
|
|
- && x.FactoryId == draft.FactoryId
|
|
|
- && x.RuleCode == created.RuleCode)
|
|
|
- .Select(x => x.Id)
|
|
|
- .FirstAsync();
|
|
|
- }
|
|
|
- if (newRuleId <= 0)
|
|
|
- throw new S8BizException("生成规则失败:无法定位新规则 id");
|
|
|
-
|
|
|
- await db.Updateable<AdoS8ConfigDraft>()
|
|
|
- .SetColumns(x => new AdoS8ConfigDraft
|
|
|
- {
|
|
|
- GeneratedRuleId = newRuleId,
|
|
|
- Status = S8ConfigDraftStatus.Generated,
|
|
|
- UpdatedAt = DateTime.Now
|
|
|
- })
|
|
|
- .Where(x => x.Id == draft.Id)
|
|
|
- .ExecuteCommandAsync();
|
|
|
-
|
|
|
- await db.Ado.CommitTranAsync();
|
|
|
-
|
|
|
- return new AdoS8ConfigDraftGenerateRuleResultDto
|
|
|
- {
|
|
|
- RuleId = newRuleId,
|
|
|
- RuleCode = created.RuleCode,
|
|
|
- DraftStatus = S8ConfigDraftStatus.Generated
|
|
|
- };
|
|
|
- }
|
|
|
- catch
|
|
|
- {
|
|
|
- await db.Ado.RollbackTranAsync();
|
|
|
- throw;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async Task<string> ResolveExistingRuleCodeAsync(long ruleId)
|
|
|
- {
|
|
|
- var rule = await _rep.Context.Queryable<AdoS8WatchRule>()
|
|
|
- .Where(x => x.Id == ruleId)
|
|
|
- .Select(x => new { x.RuleCode })
|
|
|
- .FirstAsync();
|
|
|
- return rule?.RuleCode ?? string.Empty;
|
|
|
- }
|
|
|
-
|
|
|
- private static AdoS8ConfigDraftDetailDto ToDetail(AdoS8ConfigDraft e) => new()
|
|
|
- {
|
|
|
- Id = e.Id,
|
|
|
- TenantId = e.TenantId,
|
|
|
- FactoryId = e.FactoryId,
|
|
|
- DraftCode = e.DraftCode,
|
|
|
- DraftName = e.DraftName,
|
|
|
- Status = e.Status,
|
|
|
- CurrentStep = e.CurrentStep,
|
|
|
- Mechanism = e.Mechanism,
|
|
|
- StageCode = e.StageCode,
|
|
|
- OrderFlowCode = e.OrderFlowCode,
|
|
|
- ExceptionTypeCode = e.ExceptionTypeCode,
|
|
|
- GeneratedRuleId = e.GeneratedRuleId,
|
|
|
- Remark = e.Remark,
|
|
|
- CreatedAt = e.CreatedAt,
|
|
|
- UpdatedAt = e.UpdatedAt,
|
|
|
- WizardJson = e.WizardJson
|
|
|
- };
|
|
|
-
|
|
|
- private static string? NormalizeOrNull(string? value)
|
|
|
- {
|
|
|
- if (string.IsNullOrWhiteSpace(value)) return null;
|
|
|
- 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);
|
|
|
-
|
|
|
- // CONFIG-WIZARD-T3K-ORDER-FLOW-STAGE-VALIDATE-1:销售订单链路阶段额外协议校验
|
|
|
- if (string.Equals(monitorObject.ObjectCode, "SALES_ORDER_FLOW_STAGE", StringComparison.Ordinal))
|
|
|
- {
|
|
|
- if (string.IsNullOrWhiteSpace(form.OrderFlowCode))
|
|
|
- throw new S8BizException("订单链路阶段规则必须选择订单流程节点");
|
|
|
- var ocf = NormalizeOrNull(metric.ObjectCodeField);
|
|
|
- if (!string.Equals(ocf, "order_code", StringComparison.OrdinalIgnoreCase))
|
|
|
- throw new S8BizException("订单链路阶段规则的 objectCodeField 必须为 order_code");
|
|
|
- }
|
|
|
-
|
|
|
- // 异常类型(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";
|
|
|
-
|
|
|
- // S8-STANDARD-DATASET-HARD-CUTOVER-1:取数来源改由用户在向导中显式选择治理数据集。
|
|
|
- //
|
|
|
- // 原实现取「tenant/factory 下第一个 enabled 的数据源」——那是物理连接,
|
|
|
- // 且"第一个"这种隐式选择本身就不该由服务端替用户做主。
|
|
|
- // 现在必须显式给出 dataset_code,且必须在 Catalog 中已定义。
|
|
|
- // 刻意**不做**任何"监控对象 → 数据集"的自动推断:没有真实映射时推断出来的
|
|
|
- // 就是假 Provider,规则会在启用期或运行期才炸,而不是在这里被拦下。
|
|
|
- var datasetCode = NormalizeOrNull(form.DatasetCode)
|
|
|
- ?? throw new S8BizException("当前无可用治理数据集:请在向导中选择该监控对象对应的数据集后再生成规则");
|
|
|
- if (!_datasetCatalog.IsDefined(datasetCode))
|
|
|
- throw new S8BizException($"数据集 {datasetCode} 未在目录中定义,无法生成可运行规则");
|
|
|
-
|
|
|
- // 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("规则所属场景未启用");
|
|
|
-
|
|
|
- var paramsJson = BuildParamsJson(form, labels, mechanism, metric, monitorObject, exceptionTypeCode);
|
|
|
-
|
|
|
- return new AdoS8WatchRule
|
|
|
- {
|
|
|
- TenantId = draft.TenantId,
|
|
|
- FactoryId = draft.FactoryId,
|
|
|
- RuleCode = ruleCode,
|
|
|
- SceneCode = stageCode,
|
|
|
- DatasetCode = datasetCode,
|
|
|
- WatchObjectType = monitorObject.ObjectType,
|
|
|
- RuleType = RuleTypeOf(mechanism),
|
|
|
- RuleMechanism = mechanism,
|
|
|
- StageCode = stageCode,
|
|
|
- OrderFlowCode = NormalizeOrNull(form.OrderFlowCode),
|
|
|
- SourceObjectType = monitorObject.ObjectType,
|
|
|
- Severity = severity,
|
|
|
- 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}"),
|
|
|
- };
|
|
|
-
|
|
|
- // S8-STANDARD-DATASET-HARD-CUTOVER-1:此处原有整套 SQL 生成机构已删除 ——
|
|
|
- // BuildExpression / ValidateSqlIdentifier / SafeIdentifierPattern /
|
|
|
- // BuildSoftDeletePredicate / DatePlaceholderExpression / ValuePlaceholderExpression
|
|
|
- // 它按 monitor_object.source_table + monitor_metric 字段映射拼 SELECT,是 S8 最后一处
|
|
|
- // "自己造 SQL 去读别人的业务表"。S8 是数据中台消费层,取数只经 Provider,故整块移除。
|
|
|
- // 相应地,向导现在要求显式选择 dataset_code。
|
|
|
-
|
|
|
- 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; }
|
|
|
- /// <summary>用户显式选择的治理数据集编码。生成规则的唯一取数配置。</summary>
|
|
|
- public string? DatasetCode { 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; }
|
|
|
- }
|
|
|
-}
|