|
|
@@ -229,4 +229,192 @@ WHERE tenant_id=@t AND factory_id=@f AND module_code=@m AND is_deleted=0
|
|
|
{
|
|
|
public DateTime? MaxBiz { get; set; }
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 通用指标字典(按模块)。与 s4-metric-catalog 等价,moduleCode 必填。
|
|
|
+ /// </summary>
|
|
|
+ [HttpGet("metric-catalog/{moduleCode}")]
|
|
|
+ public async Task<IActionResult> GetMetricCatalogGeneric(string moduleCode, [FromQuery] int? metricLevel = null)
|
|
|
+ {
|
|
|
+ var mc = (moduleCode ?? "").ToUpperInvariant();
|
|
|
+ if (string.IsNullOrWhiteSpace(mc)) return BadRequest(new { message = "moduleCode 必填" });
|
|
|
+ var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
|
|
|
+
|
|
|
+ var q = _db.Queryable<AdoSmartOpsKpiMaster>()
|
|
|
+ .Where(x => x.TenantId == tenantId && x.ModuleCode == mc && x.IsEnabled);
|
|
|
+ if (metricLevel is > 0)
|
|
|
+ q = q.Where(x => x.MetricLevel == metricLevel);
|
|
|
+ var list = await q.OrderBy(x => x.SortNo).ToListAsync();
|
|
|
+ return Ok(list.Select(x => new
|
|
|
+ {
|
|
|
+ id = x.Id,
|
|
|
+ metricCode = x.MetricCode,
|
|
|
+ moduleCode = x.ModuleCode,
|
|
|
+ metricLevel = x.MetricLevel,
|
|
|
+ defaultName = x.MetricName,
|
|
|
+ unit = x.Unit,
|
|
|
+ direction = x.Direction,
|
|
|
+ sortHint = x.SortNo,
|
|
|
+ parentId = x.ParentId,
|
|
|
+ description = x.Description,
|
|
|
+ formula = x.Formula,
|
|
|
+ formulaExpr = x.FormulaExpr,
|
|
|
+ formulaPreview = x.FormulaPreview,
|
|
|
+ calcRule = x.CalcRule,
|
|
|
+ dataSource = x.DataSource,
|
|
|
+ statFrequency = x.StatFrequency,
|
|
|
+ department = x.Department
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ public class GenericLayoutSaveDto
|
|
|
+ {
|
|
|
+ public List<GenericLayoutL1Dto>? L1 { get; set; }
|
|
|
+ public List<GenericLayoutL2Dto>? L2 { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class GenericLayoutL1Dto
|
|
|
+ {
|
|
|
+ public string? RowId { get; set; }
|
|
|
+ public string? MetricCode { get; set; }
|
|
|
+ public string? DisplayName { get; set; }
|
|
|
+ public int SortNo { get; set; }
|
|
|
+ public string? FormulaText { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class GenericLayoutL2Dto
|
|
|
+ {
|
|
|
+ public string? RowId { get; set; }
|
|
|
+ public string? MetricCode { get; set; }
|
|
|
+ public string? DisplayName { get; set; }
|
|
|
+ public int SortNo { get; set; }
|
|
|
+ public string? ParentRowId { get; set; }
|
|
|
+ public string? FormulaText { get; set; }
|
|
|
+ public string? PanelZone { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 通用保存运营布局:按 moduleCode 保存 L1/L2 到 LayoutItem;同步 KpiMaster.DisplayName。
|
|
|
+ /// 约束:L1 最多 MaxL1PerModule;必须命中该 moduleCode 的字典项。
|
|
|
+ /// </summary>
|
|
|
+ [HttpPut("operation-layout/{moduleCode}")]
|
|
|
+ public async Task<IActionResult> PutOperationLayoutGeneric(string moduleCode, [FromBody] GenericLayoutSaveDto? body, [FromQuery] long factoryId = 1)
|
|
|
+ {
|
|
|
+ var mc = (moduleCode ?? "").ToUpperInvariant();
|
|
|
+ if (string.IsNullOrWhiteSpace(mc)) return BadRequest(new { message = "moduleCode 必填" });
|
|
|
+ var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
|
|
|
+ if (body?.L1 == null || body.L2 == null)
|
|
|
+ return BadRequest(new { message = "L1/L2 不能为空" });
|
|
|
+ if (body.L1.Count > MaxL1PerModule)
|
|
|
+ return BadRequest(new { message = $"L1 最多 {MaxL1PerModule} 个" });
|
|
|
+
|
|
|
+ var kpiAll = await _db.Queryable<AdoSmartOpsKpiMaster>()
|
|
|
+ .Where(x => x.TenantId == tenantId && x.ModuleCode == mc && x.IsEnabled)
|
|
|
+ .ToListAsync();
|
|
|
+ var kpiByCode = kpiAll.ToDictionary(x => x.MetricCode, StringComparer.OrdinalIgnoreCase);
|
|
|
+
|
|
|
+ foreach (var r in body.L1)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(r.MetricCode) || !kpiByCode.TryGetValue(r.MetricCode.Trim(), out var k) || k.MetricLevel != 1)
|
|
|
+ return BadRequest(new { message = $"无效的 L1 指标: {r.MetricCode}" });
|
|
|
+ }
|
|
|
+
|
|
|
+ var l1Ids = new HashSet<string>(StringComparer.Ordinal);
|
|
|
+ foreach (var r in body.L1)
|
|
|
+ {
|
|
|
+ var rid = string.IsNullOrWhiteSpace(r.RowId) ? $"{mc}-L1-{r.MetricCode!.Trim()}" : r.RowId.Trim();
|
|
|
+ if (!l1Ids.Add(rid))
|
|
|
+ return BadRequest(new { message = $"重复的 L1 RowId: {rid}" });
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var r in body.L2)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(r.MetricCode) || !kpiByCode.TryGetValue(r.MetricCode.Trim(), out var k) || k.MetricLevel < 2)
|
|
|
+ return BadRequest(new { message = $"无效的 L2/L3 指标: {r.MetricCode}" });
|
|
|
+ }
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ _db.Ado.BeginTran();
|
|
|
+
|
|
|
+ await _db.Deleteable<AdoSmartOpsLayoutItem>()
|
|
|
+ .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ var rows = new List<AdoSmartOpsLayoutItem>();
|
|
|
+ foreach (var r in body.L1)
|
|
|
+ {
|
|
|
+ var rid = string.IsNullOrWhiteSpace(r.RowId) ? $"{mc}-L1-{r.MetricCode!.Trim()}" : r.RowId.Trim();
|
|
|
+ rows.Add(new AdoSmartOpsLayoutItem
|
|
|
+ {
|
|
|
+ TenantId = tenantId, FactoryId = factoryId, ModuleCode = mc,
|
|
|
+ RowId = rid, MetricLevel = 1, MetricCode = r.MetricCode!.Trim(),
|
|
|
+ DisplayName = string.IsNullOrWhiteSpace(r.DisplayName) ? null : r.DisplayName.Trim(),
|
|
|
+ SortNo = r.SortNo, ParentRowId = null,
|
|
|
+ FormulaText = string.IsNullOrWhiteSpace(r.FormulaText) ? null : r.FormulaText.Trim(),
|
|
|
+ PanelZone = null, IsEnabled = 1, UpdateTime = now
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var r in body.L2)
|
|
|
+ {
|
|
|
+ var code = r.MetricCode!.Trim();
|
|
|
+ var level = kpiByCode.TryGetValue(code, out var kk) ? kk.MetricLevel : 2;
|
|
|
+ var prefix = $"{mc}-L{level}-";
|
|
|
+ var rid = string.IsNullOrWhiteSpace(r.RowId) ? $"{prefix}{code}" : r.RowId.Trim();
|
|
|
+ var p = string.IsNullOrWhiteSpace(r.ParentRowId) ? null : r.ParentRowId.Trim();
|
|
|
+ rows.Add(new AdoSmartOpsLayoutItem
|
|
|
+ {
|
|
|
+ TenantId = tenantId, FactoryId = factoryId, ModuleCode = mc,
|
|
|
+ RowId = rid, MetricLevel = level, MetricCode = code,
|
|
|
+ DisplayName = string.IsNullOrWhiteSpace(r.DisplayName) ? null : r.DisplayName.Trim(),
|
|
|
+ SortNo = r.SortNo, ParentRowId = p,
|
|
|
+ FormulaText = string.IsNullOrWhiteSpace(r.FormulaText) ? null : r.FormulaText,
|
|
|
+ PanelZone = string.IsNullOrWhiteSpace(r.PanelZone) ? null : r.PanelZone.Trim(),
|
|
|
+ IsEnabled = 1, UpdateTime = now
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rows.Count > 0)
|
|
|
+ await _db.Insertable(rows).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ foreach (var r in body.L1.Concat<object>(body.L2).Cast<dynamic>())
|
|
|
+ {
|
|
|
+ string? code = (string?)r.MetricCode?.Trim();
|
|
|
+ string? dn = (string?)r.DisplayName?.Trim();
|
|
|
+ if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(dn)) continue;
|
|
|
+ if (!kpiByCode.TryGetValue(code, out var kpi)) continue;
|
|
|
+ if (!string.Equals(kpi.MetricName, dn, StringComparison.Ordinal))
|
|
|
+ {
|
|
|
+ await _db.Updateable<AdoSmartOpsKpiMaster>()
|
|
|
+ .SetColumns(x => new AdoSmartOpsKpiMaster { MetricName = dn, UpdatedAt = now })
|
|
|
+ .Where(x => x.Id == kpi.Id)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var hmRows = await _db.Updateable<AdoSmartOpsHomeModule>()
|
|
|
+ .SetColumns(x => new AdoSmartOpsHomeModule { UpdateTime = now })
|
|
|
+ .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ if (hmRows == 0)
|
|
|
+ {
|
|
|
+ await _db.Insertable(new AdoSmartOpsHomeModule
|
|
|
+ {
|
|
|
+ TenantId = tenantId, FactoryId = factoryId, ModuleCode = mc,
|
|
|
+ LayoutPattern = "card_grid", UpdateTime = now
|
|
|
+ }).ExecuteCommandAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ _db.Ado.CommitTran();
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _db.Ado.RollbackTran();
|
|
|
+ return BadRequest(new { message = ex.Message });
|
|
|
+ }
|
|
|
+
|
|
|
+ return Ok(new { ok = true });
|
|
|
+ }
|
|
|
}
|