S8DashboardCellConfigService.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. using Admin.NET.Plugin.AiDOP.Dto.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  4. /// <summary>
  5. /// S8 大屏卡片配置(ado_s8_dashboard_cell_config)CRUD。
  6. /// 列表合并全局基线(0/0)与工厂覆盖,同 (pageCode, cellCode) 以工厂记录优先。
  7. /// </summary>
  8. public class S8DashboardCellConfigService : ITransient
  9. {
  10. private readonly SqlSugarRepository<AdoS8DashboardCellConfig> _rep;
  11. public S8DashboardCellConfigService(SqlSugarRepository<AdoS8DashboardCellConfig> rep) => _rep = rep;
  12. public async Task<List<AdoS8DashboardCellConfig>> ListAsync(long tenantId, long factoryId)
  13. {
  14. var all = await _rep.AsQueryable()
  15. .Where(x => (x.TenantId == 0 && x.FactoryId == 0)
  16. || (x.TenantId == tenantId && x.FactoryId == factoryId))
  17. .ToListAsync();
  18. return all
  19. .GroupBy(x => (x.PageCode, x.CellCode))
  20. .Select(g => g.OrderByDescending(x => x.FactoryId).First())
  21. .OrderBy(x => x.PageCode)
  22. .ThenBy(x => x.SortNo)
  23. .ThenBy(x => x.CellCode)
  24. .ToList();
  25. }
  26. public async Task<AdoS8DashboardCellConfig> CreateAsync(AdoS8DashboardCellConfig body)
  27. {
  28. ValidateAndNormalize(body);
  29. var exists = await _rep.AsQueryable()
  30. .AnyAsync(x => x.TenantId == body.TenantId && x.FactoryId == body.FactoryId
  31. && x.PageCode == body.PageCode && x.CellCode == body.CellCode);
  32. if (exists) throw new S8BizException("同一工厂下页面与卡片编码组合已存在");
  33. body.Id = 0;
  34. body.CreatedAt = DateTime.Now;
  35. body.UpdatedAt = null;
  36. await _rep.InsertAsync(body);
  37. return body;
  38. }
  39. public async Task<AdoS8DashboardCellConfig> UpdateAsync(long id, AdoS8DashboardCellConfig body)
  40. {
  41. var e = await _rep.GetByIdAsync(id) ?? throw new S8BizException("记录不存在");
  42. ValidateAndNormalize(body);
  43. var dup = await _rep.AsQueryable()
  44. .AnyAsync(x => x.Id != id && x.TenantId == body.TenantId && x.FactoryId == body.FactoryId
  45. && x.PageCode == body.PageCode && x.CellCode == body.CellCode);
  46. if (dup) throw new S8BizException("同一工厂下页面与卡片编码组合已存在");
  47. body.Id = id;
  48. body.CreatedAt = e.CreatedAt;
  49. body.UpdatedAt = DateTime.Now;
  50. await _rep.UpdateAsync(body);
  51. return body;
  52. }
  53. public async Task DeleteAsync(long id) => await _rep.DeleteByIdAsync(id);
  54. /// <summary>
  55. /// 获取指定页面的渲染配置(G-09 一期)。
  56. /// 合并规则:全局基线 (0/0) + 工厂覆盖 (tenantId/factoryId),同 cell_code 工厂优先;
  57. /// 过滤规则:合并后再过滤 enabled=true(禁止读取 override 时提前过滤 enabled);
  58. /// 排序规则:layout_area(MODULES→ANALYSIS→SIDEBAR)→ sort_no → cell_code。
  59. /// 本接口只读 ado_s8_dashboard_cell_config,不触发 ado_s8_exception 查询。
  60. /// </summary>
  61. public async Task<AdoS8PageConfigDto> GetPageConfigAsync(AdoS8PageConfigQueryDto q)
  62. {
  63. // 1. pageCode 严格校验(大写精确匹配,非法返回 400)
  64. if (string.IsNullOrWhiteSpace(q.PageCode))
  65. throw new S8BizException("pageCode 不能为空");
  66. var allowed = new[] { "OVERVIEW", "DELIVERY", "PRODUCTION", "SUPPLY" };
  67. if (!allowed.Contains(q.PageCode))
  68. throw new S8BizException($"Invalid pageCode: {q.PageCode}");
  69. // 2. 取全局基线(不过滤 enabled)
  70. var baseline = await _rep.AsQueryable()
  71. .Where(x => x.TenantId == 0 && x.FactoryId == 0 && x.PageCode == q.PageCode)
  72. .ToListAsync();
  73. // 3. 取工厂覆盖(若有;同样不过滤 enabled,否则工厂显式关闭的卡会被基线重新激活)
  74. var overrides = (q.TenantId != 0 || q.FactoryId != 0)
  75. ? await _rep.AsQueryable()
  76. .Where(x => x.TenantId == q.TenantId && x.FactoryId == q.FactoryId
  77. && x.PageCode == q.PageCode)
  78. .ToListAsync()
  79. : new List<AdoS8DashboardCellConfig>();
  80. // 4. 按 cell_code 合并,工厂覆盖优先
  81. var merged = new Dictionary<string, AdoS8DashboardCellConfig>();
  82. foreach (var cfg in baseline) merged[cfg.CellCode] = cfg;
  83. foreach (var cfg in overrides) merged[cfg.CellCode] = cfg;
  84. // 5. 合并后再过滤 enabled=true,然后按 layout_area → sort_no → cell_code 排序
  85. var ordered = merged.Values
  86. .Where(c => c.Enabled)
  87. .OrderBy(c => LayoutAreaOrder(c.LayoutArea))
  88. .ThenBy(c => c.SortNo)
  89. .ThenBy(c => c.CellCode)
  90. .ToList();
  91. return new AdoS8PageConfigDto
  92. {
  93. PageCode = q.PageCode,
  94. Cells = ordered.Select(ToCellDto).ToList(),
  95. };
  96. }
  97. private static int LayoutAreaOrder(string? area) => (area ?? string.Empty).ToUpperInvariant() switch
  98. {
  99. "MODULES" => 1,
  100. "ANALYSIS" => 2,
  101. "SIDEBAR" => 3,
  102. _ => 99,
  103. };
  104. private static AdoS8PageConfigCellDto ToCellDto(AdoS8DashboardCellConfig c) => new()
  105. {
  106. CellCode = c.CellCode,
  107. CellTitle = c.CellTitle,
  108. Icon = c.Icon,
  109. LayoutArea = string.IsNullOrWhiteSpace(c.LayoutArea) ? "ANALYSIS" : c.LayoutArea,
  110. DisplayMode = string.IsNullOrWhiteSpace(c.DisplayMode) ? "CATEGORY_CARD" : c.DisplayMode,
  111. SortNo = c.SortNo,
  112. BindingType = c.BindingType,
  113. ExceptionTypeCode = c.ExceptionTypeCode,
  114. AggregateScope = c.AggregateScope,
  115. StatMetric = c.StatMetric,
  116. TimeWindow = c.TimeWindow,
  117. DeptGroupBy = c.DeptGroupBy,
  118. Enabled = c.Enabled,
  119. // 注意:不映射 ShowInSidebar / FilterExpression / TenantId / FactoryId / Id / CreatedAt / UpdatedAt
  120. };
  121. private static void ValidateAndNormalize(AdoS8DashboardCellConfig body)
  122. {
  123. if (string.IsNullOrWhiteSpace(body.PageCode) || string.IsNullOrWhiteSpace(body.CellCode))
  124. throw new S8BizException("页面编码与卡片编码必填");
  125. body.PageCode = body.PageCode.Trim();
  126. body.CellCode = body.CellCode.Trim();
  127. if (body.CellTitle != null) body.CellTitle = body.CellTitle.Trim();
  128. if (string.IsNullOrWhiteSpace(body.BindingType))
  129. body.BindingType = "CUSTOM";
  130. body.BindingType = body.BindingType.Trim().ToUpperInvariant();
  131. if (body.BindingType is not ("EXCEPTION_TYPE" or "AGGREGATE" or "CUSTOM"))
  132. throw new S8BizException("绑定类型须为 EXCEPTION_TYPE / AGGREGATE / CUSTOM");
  133. switch (body.BindingType)
  134. {
  135. case "EXCEPTION_TYPE":
  136. if (string.IsNullOrWhiteSpace(body.ExceptionTypeCode))
  137. throw new S8BizException("绑定类型为异常类型时须填写异常类型编码");
  138. body.ExceptionTypeCode = body.ExceptionTypeCode.Trim();
  139. body.AggregateScope = null;
  140. break;
  141. case "AGGREGATE":
  142. if (string.IsNullOrWhiteSpace(body.AggregateScope))
  143. throw new S8BizException("绑定类型为域聚合时须选择聚合范围");
  144. body.AggregateScope = body.AggregateScope!.Trim();
  145. body.ExceptionTypeCode = null;
  146. break;
  147. default:
  148. body.ExceptionTypeCode = null;
  149. body.AggregateScope = null;
  150. break;
  151. }
  152. if (string.IsNullOrWhiteSpace(body.StatMetric)) body.StatMetric = "OPEN_COUNT";
  153. body.StatMetric = body.StatMetric.Trim().ToUpperInvariant();
  154. if (body.StatMetric is not ("OPEN_COUNT" or "FREQUENCY" or "AVG_DURATION" or "CLOSE_RATE"))
  155. throw new S8BizException("统计指标须为 OPEN_COUNT / FREQUENCY / AVG_DURATION / CLOSE_RATE");
  156. if (string.IsNullOrWhiteSpace(body.TimeWindow)) body.TimeWindow = "LAST_24H";
  157. body.TimeWindow = body.TimeWindow.Trim().ToUpperInvariant();
  158. if (body.TimeWindow is not ("TODAY" or "LAST_24H" or "LAST_7D" or "LAST_30D"))
  159. throw new S8BizException("时间窗须为 TODAY / LAST_24H / LAST_7D / LAST_30D");
  160. if (string.IsNullOrWhiteSpace(body.DeptGroupBy)) body.DeptGroupBy = "OWNER";
  161. body.DeptGroupBy = body.DeptGroupBy.Trim().ToUpperInvariant();
  162. if (body.DeptGroupBy is not ("OWNER" or "OCCUR"))
  163. throw new S8BizException("部门聚合维度须为 OWNER(责任部门)或 OCCUR(发生部门)");
  164. if (body.FilterExpression != null && body.FilterExpression.Length > 1000)
  165. throw new S8BizException("筛选表达式过长");
  166. }
  167. }