using Admin.NET.Plugin.AiDOP.Dto.S8; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// S9 KPI 目标值配置服务(S8-CONFIG-GLOBAL-ROW-SEMANTICS-AND-KPI-TARGET-1)。 /// /// 复用 ado_s8_monitor_metric,不新建配置表;只维护 default_target_ratio(目标值)。 /// 作用域语义与步骤 1 完全一致:平台默认(0/0)租户侧只读,需要调整走「自定义本工厂」覆盖, /// 「恢复平台默认」只删覆盖行。生效读取:工厂覆盖 > 平台默认。 /// /// 边界: /// - 只暴露 Result KPI(mechanism='RATIO' AND is_result_kpi=1),当前 5 项; /// 表里其余 8 行是监控指标字典,不属于业务目标值配置,不向业务前端暴露。 /// - 只配置 TargetRatio。KPI 当前值(CurrentValue)不是配置项,由业务计算/接口提供,本服务不涉及。 /// public class S8KpiTargetConfigService : ITransient { /// 目标值为百分比,合法区间 [0, 100]。 public const decimal MinTargetRatio = 0m; public const decimal MaxTargetRatio = 100m; private readonly SqlSugarRepository _rep; public S8KpiTargetConfigService(SqlSugarRepository rep) => _rep = rep; /// Result KPI 判定:与 同口径。 private ISugarQueryable ResultKpiQuery() => _rep.AsQueryable().Where(x => x.Mechanism == "RATIO" && x.IsResultKpi); /// /// 列表:返回 5 项 Result KPI 的「生效目标值」视图。 /// 有工厂覆盖 → Scope=FACTORY 且 TargetRatio 取覆盖值;否则 Scope=GLOBAL 取平台默认值。 /// 不返回 TenantId / FactoryId / data_source 等技术字段。 /// public async Task> ListAsync(S8TrustedScope scope) { var rows = await ResultKpiQuery() .Where(x => (x.TenantId == S8ConfigScope.GlobalTenantId && x.FactoryId == S8ConfigScope.GlobalFactoryId) || (x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)) .ToListAsync(); var globals = rows .Where(x => S8ConfigScope.IsGlobal(x.TenantId, x.FactoryId)) .ToDictionary(x => x.MetricCode); var overrides = rows .Where(x => !S8ConfigScope.IsGlobal(x.TenantId, x.FactoryId)) .ToDictionary(x => x.MetricCode); // 以平台默认行为骨架:平台没有定义的指标不应凭工厂覆盖凭空出现。 return globals.Values .OrderBy(x => x.SortNo).ThenBy(x => x.MetricCode) .Select(g => { overrides.TryGetValue(g.MetricCode, out var ov); return new AdoS8KpiTargetItemDto { MetricCode = g.MetricCode, MetricName = g.MetricName, Unit = string.IsNullOrWhiteSpace(g.Unit) ? "%" : g.Unit!, TargetRatio = ov?.DefaultTargetRatio ?? g.DefaultTargetRatio, GlobalTargetRatio = g.DefaultTargetRatio, Remark = ov?.Remark ?? g.Remark, Scope = ov != null ? S8ConfigScope.Factory : S8ConfigScope.Global, HasFactoryOverride = ov != null, }; }) .ToList(); } /// /// 自定义本工厂 / 编辑本工厂目标值(同一入口,按业务键 metricCode 定位,不暴露 DB Id)。 /// 无覆盖行 → 复制平台默认并盖章可信作用域后新建;已有覆盖行 → 就地更新目标值。 /// 归属列一律由服务端盖章,不接受客户端传入。 /// public async Task UpsertFactoryOverrideAsync( string metricCode, decimal targetRatio, string? remark, S8TrustedScope scope) { if (string.IsNullOrWhiteSpace(metricCode)) throw new S8BizException("指标编码必填"); ValidateTarget(targetRatio); var code = metricCode.Trim(); var global = await ResultKpiQuery() .Where(x => x.MetricCode == code && x.TenantId == S8ConfigScope.GlobalTenantId && x.FactoryId == S8ConfigScope.GlobalFactoryId) .FirstAsync() ?? throw new S8NotFoundException("平台默认指标不存在"); var existing = await _rep.AsQueryable() .Where(x => x.MetricCode == code && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId) .FirstAsync(); if (existing != null) { existing.DefaultTargetRatio = targetRatio; if (remark != null) existing.Remark = NormalizeOrNull(remark); existing.UpdatedAt = DateTime.Now; await _rep.UpdateAsync(existing); } else { var copy = CloneForFactory(global, scope); copy.DefaultTargetRatio = targetRatio; if (remark != null) copy.Remark = NormalizeOrNull(remark); copy.Id = await _rep.AsInsertable(copy).ExecuteReturnBigIdentityAsync(); } return (await ListAsync(scope)).First(x => x.MetricCode == code); } /// /// 恢复平台默认:只删除当前工厂覆盖行,平台默认行不受影响;删除后读取自动回落平台默认。 /// 本就没有覆盖行时按「不存在」处理(幂等语义由调用方 404 表达)。 /// public async Task ResetToGlobalDefaultAsync(string metricCode, S8TrustedScope scope) { if (string.IsNullOrWhiteSpace(metricCode)) throw new S8BizException("指标编码必填"); var code = metricCode.Trim(); var existing = await _rep.AsQueryable() .Where(x => x.MetricCode == code && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId) .FirstAsync() ?? throw new S8NotFoundException("当前工厂没有覆盖配置,无需恢复"); await _rep.DeleteByIdAsync(existing.Id); return (await ListAsync(scope)).First(x => x.MetricCode == code); } private static void ValidateTarget(decimal targetRatio) { if (targetRatio < MinTargetRatio || targetRatio > MaxTargetRatio) throw new S8BizException($"目标值必须在 {MinTargetRatio:0}–{MaxTargetRatio:0} 之间"); } private static string? NormalizeOrNull(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim(); /// 复制平台默认行的字典字段,归属由服务端盖章;不复制 Id / 时间戳。 private static AdoS8MonitorMetric CloneForFactory(AdoS8MonitorMetric g, S8TrustedScope scope) => new() { Id = 0, TenantId = scope.TenantId, FactoryId = scope.FactoryId, ObjectCode = g.ObjectCode, MetricCode = g.MetricCode, MetricName = g.MetricName, Mechanism = g.Mechanism, Unit = g.Unit, DueAtField = g.DueAtField, StatusField = g.StatusField, MeasuredValueField = g.MeasuredValueField, ObjectIdField = g.ObjectIdField, ObjectCodeField = g.ObjectCodeField, ObjectNameField = g.ObjectNameField, DefaultGraceMinutes = g.DefaultGraceMinutes, DefaultCompletedStates = g.DefaultCompletedStates, DefaultTargetRatio = g.DefaultTargetRatio, DefaultLowerBound = g.DefaultLowerBound, DefaultUpperBound = g.DefaultUpperBound, IsResultKpi = g.IsResultKpi, Enabled = g.Enabled, SortNo = g.SortNo, Remark = g.Remark, CreatedAt = DateTime.Now, UpdatedAt = null, }; }