AidopKanbanController.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Admin.NET.Plugin.AiDOP.Production;
  5. using SqlSugar;
  6. namespace Admin.NET.Plugin.AiDOP.Controllers;
  7. [ApiController]
  8. [Route("api/[controller]")]
  9. [AllowAnonymous]
  10. [NonUnify]
  11. public partial class AidopKanbanController : ControllerBase
  12. {
  13. private readonly ISqlSugarClient _db;
  14. private readonly S2MdpSyncTransformService _s2MdpSyncTransformService;
  15. public AidopKanbanController(ISqlSugarClient db, S2MdpSyncTransformService s2MdpSyncTransformService)
  16. {
  17. _db = db;
  18. _s2MdpSyncTransformService = s2MdpSyncTransformService;
  19. }
  20. [HttpGet("home-l1")]
  21. public async Task<IActionResult> GetHomeL1([FromQuery] long factoryId = 1)
  22. {
  23. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  24. const string sql = """
  25. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode,
  26. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  27. v.status_color AS StatusColor, v.trend_flag AS TrendFlag,
  28. k.Direction, k.YellowThreshold, k.RedThreshold
  29. FROM ado_s9_kpi_value_l1_day v
  30. LEFT JOIN ado_smart_ops_kpi_master k
  31. ON k.MetricCode = v.metric_code COLLATE utf8mb4_general_ci AND k.TenantId = v.tenant_id
  32. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.is_deleted=0
  33. AND v.biz_date = (SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
  34. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0)
  35. ORDER BY v.module_code
  36. """;
  37. var raw = await _db.Ado.SqlQueryAsync<HomeL1RawDto>(sql, new { tenantId, factoryId });
  38. var rows = raw.Select(r => new HomeL1Dto
  39. {
  40. ModuleCode = r.ModuleCode,
  41. MetricCode = r.MetricCode,
  42. MetricValue = r.MetricValue,
  43. TargetValue = r.TargetValue,
  44. TrendFlag = r.TrendFlag,
  45. StatusColor = AidopS4KpiMerge.AchievementLevel(
  46. r.MetricValue, r.TargetValue,
  47. r.Direction ?? "higher_is_better",
  48. r.YellowThreshold, r.RedThreshold)
  49. }).ToList();
  50. return Ok(rows);
  51. }
  52. [HttpGet("s8-alerts")]
  53. public async Task<IActionResult> GetS8Alerts([FromQuery] long factoryId = 1)
  54. {
  55. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  56. const string sql = """
  57. SELECT DATE_FORMAT(alert_time, '%H:%i') AS Time,
  58. IFNULL(SUBSTRING_INDEX(metric_code, '_', 1), 'S8') AS Module,
  59. IFNULL(message, '异常告警') AS Message,
  60. IFNULL(level_code, 'medium') AS LevelCode
  61. FROM ado_s8_alert_record
  62. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  63. ORDER BY alert_time DESC
  64. LIMIT 6
  65. """;
  66. var rows = await _db.Ado.SqlQueryAsync<S8AlertDto>(sql, new { tenantId, factoryId });
  67. return Ok(rows.Select(x => new
  68. {
  69. time = x.Time,
  70. module = x.Module,
  71. message = x.Message,
  72. level = x.LevelCode,
  73. levelLabel = x.LevelCode switch
  74. {
  75. "critical" => "严重",
  76. "high" => "高",
  77. "medium" => "中",
  78. _ => "一般"
  79. }
  80. }));
  81. }
  82. [HttpGet("module-detail")]
  83. public async Task<IActionResult> GetModuleDetail([FromQuery] string moduleCode = "S1", [FromQuery] long factoryId = 1)
  84. {
  85. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  86. moduleCode = string.IsNullOrWhiteSpace(moduleCode) ? "S1" : moduleCode.ToUpperInvariant();
  87. const string l2Sql = """
  88. SELECT module_code AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  89. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  90. FROM ado_s9_kpi_value_l2_day
  91. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0 AND module_code=@moduleCode
  92. ORDER BY id DESC, metric_code
  93. LIMIT 30
  94. """;
  95. const string l3Sql = """
  96. SELECT module_code AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  97. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  98. FROM ado_s9_kpi_value_l3_day
  99. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0 AND module_code=@moduleCode
  100. ORDER BY id DESC, metric_code
  101. LIMIT 60
  102. """;
  103. const string alertSql = """
  104. SELECT DATE_FORMAT(alert_time, '%H:%i:%s') AS Time,
  105. IFNULL(level_code, 'medium') AS LevelCode,
  106. IFNULL(message, '异常告警') AS Message
  107. FROM ado_s8_alert_record
  108. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  109. AND (UPPER(IFNULL(module_code,''))=@moduleCode OR UPPER(IFNULL(SUBSTRING_INDEX(metric_code, '_', 1), ''))=@moduleCode)
  110. ORDER BY alert_time DESC
  111. LIMIT 20
  112. """;
  113. const string l2FallbackSql = """
  114. SELECT '' AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  115. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  116. FROM ado_s9_kpi_value_l2_day
  117. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  118. ORDER BY id DESC
  119. LIMIT 30
  120. """;
  121. const string l3FallbackSql = """
  122. SELECT '' AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  123. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  124. FROM ado_s9_kpi_value_l3_day
  125. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  126. ORDER BY id DESC
  127. LIMIT 60
  128. """;
  129. var l2 = new List<KpiDetailDto>();
  130. var l3 = new List<KpiDetailDto>();
  131. var alerts = new List<S8AlertDto>();
  132. try { l2 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l2Sql, new { moduleCode, tenantId, factoryId }); }
  133. catch { l2 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l2FallbackSql, new { tenantId, factoryId }); }
  134. try { l3 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l3Sql, new { moduleCode, tenantId, factoryId }); }
  135. catch { l3 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l3FallbackSql, new { tenantId, factoryId }); }
  136. try { alerts = await _db.Ado.SqlQueryAsync<S8AlertDto>(alertSql, new { moduleCode, tenantId, factoryId }); }
  137. catch { alerts = new List<S8AlertDto>(); }
  138. var schedules = new List<S2ScheduleDto>();
  139. var s2Alerts = new List<S8AlertDto>();
  140. var decomposition = new List<S2DecompositionDto>();
  141. var trend = new List<S2TrendDto>();
  142. var distribution = new List<S2DistributionDto>();
  143. S2SyncStatusDto? syncStatus = null;
  144. if (moduleCode == "S2")
  145. {
  146. syncStatus = await GetS2SyncStatusAsync();
  147. s2Alerts = await GetS2DerivedAlertsAsync(tenantId, factoryId);
  148. if (s2Alerts.Count > 0)
  149. alerts = s2Alerts;
  150. decomposition = await GetS2DecompositionAsync(tenantId, factoryId, l2);
  151. trend = await GetS2TrendAsync(tenantId, factoryId);
  152. distribution = await GetS2DistributionAsync(tenantId, factoryId);
  153. try
  154. {
  155. schedules = await _db.Ado.SqlQueryAsync<S2ScheduleDto>(
  156. """
  157. SELECT work_order AS OrderNo,
  158. COALESCE(NULLIF(item_name, ''), item_code, work_order) AS Product,
  159. COALESCE(NULLIF(sales_order_no, ''), work_order) AS Customer,
  160. prod_line AS ProductionLine,
  161. qty_ordered AS Quantity,
  162. due_date AS DeliveryDate,
  163. first_start_time AS StartTime,
  164. schedule_cycle_days AS CycleDays,
  165. CASE WHEN schedule_satisfaction_flag = 1 THEN 100
  166. WHEN due_date IS NULL OR last_plan_date IS NULL THEN 70
  167. ELSE 65 END AS Satisfaction,
  168. CASE WHEN schedule_satisfaction_flag = 1 THEN '已锁定'
  169. WHEN last_plan_date IS NULL THEN '待排程'
  170. ELSE '资源异常' END AS Status
  171. FROM dwd_order_schedule_trans
  172. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  173. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  174. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  175. ORDER BY calc_time DESC, id DESC LIMIT 1)
  176. ORDER BY urgent_flag DESC, due_date IS NULL, due_date, id DESC
  177. LIMIT 30
  178. """,
  179. new { tenantId, factoryId });
  180. }
  181. catch
  182. {
  183. schedules = new List<S2ScheduleDto>();
  184. }
  185. }
  186. return Ok(new
  187. {
  188. moduleCode,
  189. l2,
  190. l3,
  191. syncStatus,
  192. decomposition,
  193. trend,
  194. distribution,
  195. schedules,
  196. alerts = alerts.Select(x => new
  197. {
  198. time = x.Time,
  199. message = x.Message,
  200. level = x.LevelCode
  201. })
  202. });
  203. }
  204. [HttpPost("s2-mdp/refresh")]
  205. public async Task<IActionResult> RefreshS2Mdp(CancellationToken cancellationToken)
  206. {
  207. var result = await _s2MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
  208. return Ok(new
  209. {
  210. ok = true,
  211. result.BatchId,
  212. result.StageRows,
  213. result.StandardRows,
  214. result.DwdRows,
  215. result.KpiRows
  216. });
  217. }
  218. private async Task<S2SyncStatusDto?> GetS2SyncStatusAsync()
  219. {
  220. try
  221. {
  222. return await _db.Ado.SqlQuerySingleAsync<S2SyncStatusDto>(
  223. """
  224. SELECT batch_id AS BatchId, status AS Status, stage_rows AS StageRows,
  225. standard_rows AS StandardRows, dwd_rows AS DwdRows,
  226. start_time AS StartTime, end_time AS EndTime, error_message AS ErrorMessage
  227. FROM mdp_transform_run_log
  228. WHERE job_code='S2_MDP_SYNC_TRANSFORM'
  229. ORDER BY start_time DESC, id DESC
  230. LIMIT 1
  231. """);
  232. }
  233. catch
  234. {
  235. return null;
  236. }
  237. }
  238. private async Task<List<S2DecompositionDto>> GetS2DecompositionAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
  239. {
  240. var latestL1 = new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
  241. try
  242. {
  243. var rows = await _db.Ado.SqlQueryAsync<KpiDetailDto>(
  244. """
  245. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode, k.MetricName AS MetricName,
  246. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  247. v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate
  248. FROM ado_s9_kpi_value_l1_day v
  249. LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code
  250. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.module_code='S2' AND v.is_deleted=0
  251. AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
  252. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code='S2' AND is_deleted=0)
  253. """,
  254. new { tenantId, factoryId });
  255. latestL1 = rows.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  256. .ToDictionary(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase);
  257. }
  258. catch
  259. {
  260. latestL1 = new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
  261. }
  262. var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  263. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  264. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  265. var operationSummary = await GetS2OperationSummaryAsync(tenantId, factoryId);
  266. var resourceSummary = await GetS2ResourceSummaryAsync(tenantId, factoryId);
  267. return new List<S2DecompositionDto>
  268. {
  269. new()
  270. {
  271. Title = "订单排程",
  272. Active = true,
  273. Metrics = new List<string>
  274. {
  275. $"1. 周期:{FormatMetric(latestL1, "S2_L1_001", "天")}",
  276. $"2. 满足率:{FormatMetric(latestL1, "S2_L1_002", "%")}",
  277. $"3. 在制库存:{FormatMetric(latestL1, "S2_L1_004", "天")}",
  278. $"4. 人效:{FormatMetric(latestL1, "S2_L1_003", "单/人")}"
  279. }
  280. },
  281. new()
  282. {
  283. Title = "工单排程",
  284. Metrics = new List<string>
  285. {
  286. $"1. 周期:{FormatMetric(latestL2, "S2_L2_001", "天")}",
  287. $"2. 满足率:{FormatMetric(latestL2, "S2_L2_002", "%")}",
  288. $"3. 人效:{FormatMetric(latestL2, "S2_L2_003", "单/人")}"
  289. }
  290. },
  291. new()
  292. {
  293. Title = "工序排程",
  294. Metrics = new List<string>
  295. {
  296. $"1. 工序数:{operationSummary.OperationCount}",
  297. $"2. 完成量:{Math.Round(operationSummary.CompletedQty ?? 0, 2)}",
  298. $"3. 排程量:{Math.Round(operationSummary.ScheduledQty ?? 0, 2)}"
  299. }
  300. },
  301. new()
  302. {
  303. Title = "资源排程",
  304. Metrics = new List<string>
  305. {
  306. $"1. 人员数:{Math.Round(resourceSummary.PersonCount ?? 0, 2)}",
  307. $"2. 异常数:{resourceSummary.RiskCount}",
  308. $"3. 产线数:{resourceSummary.LineCount}"
  309. }
  310. }
  311. };
  312. }
  313. private async Task<S2OperationSummaryDto> GetS2OperationSummaryAsync(long tenantId, long factoryId)
  314. {
  315. try
  316. {
  317. return await _db.Ado.SqlQuerySingleAsync<S2OperationSummaryDto>(
  318. """
  319. SELECT SUM(operation_count) AS OperationCount,
  320. SUM(completed_op_qty) AS CompletedQty,
  321. SUM(scheduled_qty) AS ScheduledQty
  322. FROM dwd_order_schedule_trans
  323. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  324. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  325. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  326. ORDER BY calc_time DESC, id DESC LIMIT 1)
  327. """,
  328. new { tenantId, factoryId }) ?? new S2OperationSummaryDto();
  329. }
  330. catch
  331. {
  332. return new S2OperationSummaryDto();
  333. }
  334. }
  335. private async Task<S2ResourceSummaryDto> GetS2ResourceSummaryAsync(long tenantId, long factoryId)
  336. {
  337. try
  338. {
  339. return await _db.Ado.SqlQuerySingleAsync<S2ResourceSummaryDto>(
  340. """
  341. SELECT SUM(resource_person_count) AS PersonCount,
  342. SUM(CASE WHEN schedule_satisfaction_flag=0 THEN 1 ELSE 0 END) AS RiskCount,
  343. COUNT(DISTINCT COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配')) AS LineCount
  344. FROM dwd_order_schedule_trans
  345. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  346. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  347. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  348. ORDER BY calc_time DESC, id DESC LIMIT 1)
  349. """,
  350. new { tenantId, factoryId }) ?? new S2ResourceSummaryDto();
  351. }
  352. catch
  353. {
  354. return new S2ResourceSummaryDto();
  355. }
  356. }
  357. private async Task<List<S2TrendDto>> GetS2TrendAsync(long tenantId, long factoryId)
  358. {
  359. try
  360. {
  361. return await _db.Ado.SqlQueryAsync<S2TrendDto>(
  362. """
  363. SELECT DATE_FORMAT(d.biz_date, '%m-%d') AS DateLabel,
  364. MAX(CASE WHEN d.metric_code='S2_L1_001' THEN d.metric_value END) AS CycleDays,
  365. MAX(CASE WHEN d.metric_code='S2_L1_002' THEN d.metric_value END) AS SatisfactionPct
  366. FROM ado_s9_kpi_value_l1_day d
  367. WHERE d.tenant_id=@tenantId AND d.factory_id=@factoryId AND d.module_code='S2' AND d.is_deleted=0
  368. AND d.metric_code IN ('S2_L1_001','S2_L1_002')
  369. GROUP BY d.biz_date
  370. ORDER BY d.biz_date DESC
  371. LIMIT 7
  372. """,
  373. new { tenantId, factoryId });
  374. }
  375. catch
  376. {
  377. return new List<S2TrendDto>();
  378. }
  379. }
  380. private async Task<List<S2DistributionDto>> GetS2DistributionAsync(long tenantId, long factoryId)
  381. {
  382. try
  383. {
  384. return await _db.Ado.SqlQueryAsync<S2DistributionDto>(
  385. """
  386. SELECT COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配') AS Name,
  387. ROUND(100 * SUM(schedule_satisfaction_flag) / NULLIF(COUNT(1), 0), 2) AS SatisfactionPct,
  388. COUNT(1) AS TotalCount,
  389. SUM(CASE WHEN schedule_satisfaction_flag=0 THEN 1 ELSE 0 END) AS RiskCount
  390. FROM dwd_order_schedule_trans
  391. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  392. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  393. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  394. ORDER BY calc_time DESC, id DESC LIMIT 1)
  395. GROUP BY COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配')
  396. ORDER BY SatisfactionPct, TotalCount DESC
  397. LIMIT 8
  398. """,
  399. new { tenantId, factoryId });
  400. }
  401. catch
  402. {
  403. return new List<S2DistributionDto>();
  404. }
  405. }
  406. private async Task<List<S8AlertDto>> GetS2DerivedAlertsAsync(long tenantId, long factoryId)
  407. {
  408. try
  409. {
  410. return await _db.Ado.SqlQueryAsync<S8AlertDto>(
  411. """
  412. SELECT DATE_FORMAT(calc_time, '%H:%i:%s') AS Time,
  413. CASE
  414. WHEN last_plan_date IS NULL THEN 'high'
  415. WHEN due_date IS NOT NULL AND last_plan_date > due_date THEN 'critical'
  416. WHEN IFNULL(resource_person_count, 0) <= 0 THEN 'medium'
  417. ELSE 'info'
  418. END AS LevelCode,
  419. CONCAT('工单 ', work_order, ' ',
  420. CASE
  421. WHEN last_plan_date IS NULL THEN '尚未形成排程'
  422. WHEN due_date IS NOT NULL AND last_plan_date > due_date THEN '排程晚于交期'
  423. WHEN IFNULL(resource_person_count, 0) <= 0 THEN '缺少资源人员配置'
  424. ELSE '排程状态正常'
  425. END) AS Message
  426. FROM dwd_order_schedule_trans
  427. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  428. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  429. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  430. ORDER BY calc_time DESC, id DESC LIMIT 1)
  431. AND (last_plan_date IS NULL OR (due_date IS NOT NULL AND last_plan_date > due_date) OR IFNULL(resource_person_count, 0) <= 0)
  432. ORDER BY FIELD(LevelCode, 'critical', 'high', 'medium', 'info'), due_date IS NULL, due_date
  433. LIMIT 20
  434. """,
  435. new { tenantId, factoryId });
  436. }
  437. catch
  438. {
  439. return new List<S8AlertDto>();
  440. }
  441. }
  442. private static string FormatMetric(Dictionary<string, KpiDetailDto> rows, string metricCode, string unit)
  443. {
  444. if (!rows.TryGetValue(metricCode, out var row) || row.MetricValue == null)
  445. return $"--{unit}";
  446. return $"{Math.Round(row.MetricValue.Value, 2)}{unit}";
  447. }
  448. /// <summary>
  449. /// 智慧运营看板基础查询下拉:产品、订单号、产线(来自 Demo 业务表;无租户列时忽略 tenant/factory)。
  450. /// </summary>
  451. [HttpGet("smart-ops-filter-options")]
  452. public async Task<IActionResult> GetSmartOpsFilterOptions([FromQuery] long factoryId = 1)
  453. {
  454. _ = factoryId;
  455. var products = new HashSet<string>(StringComparer.Ordinal);
  456. var orderNos = new HashSet<string>(StringComparer.Ordinal);
  457. var lines = new HashSet<string>(StringComparer.Ordinal);
  458. try
  459. {
  460. var op = await _db.Queryable<AdoOrder>()
  461. .Where(x => x.Product != null && x.Product != "")
  462. .Select(x => x.Product)
  463. .Distinct()
  464. .ToListAsync();
  465. foreach (var s in op.Where(s => !string.IsNullOrWhiteSpace(s)))
  466. products.Add(s.Trim());
  467. }
  468. catch
  469. {
  470. // ignored
  471. }
  472. try
  473. {
  474. var on = await _db.Queryable<AdoOrder>()
  475. .Where(x => x.OrderNo != null && x.OrderNo != "")
  476. .Select(x => x.OrderNo)
  477. .Distinct()
  478. .ToListAsync();
  479. foreach (var s in on.Where(s => !string.IsNullOrWhiteSpace(s)))
  480. orderNos.Add(s.Trim());
  481. }
  482. catch
  483. {
  484. // ignored
  485. }
  486. try
  487. {
  488. var woP = await _db.Queryable<AdoWorkOrder>()
  489. .Where(x => x.Product != null && x.Product != "")
  490. .Select(x => x.Product)
  491. .Distinct()
  492. .ToListAsync();
  493. foreach (var s in woP.Where(s => !string.IsNullOrWhiteSpace(s)))
  494. products.Add(s.Trim());
  495. var wc = await _db.Queryable<AdoWorkOrder>()
  496. .Where(x => x.WorkCenter != null && x.WorkCenter != "")
  497. .Select(x => x.WorkCenter)
  498. .Distinct()
  499. .ToListAsync();
  500. foreach (var s in wc.Where(s => !string.IsNullOrWhiteSpace(s)))
  501. lines.Add(s.Trim());
  502. }
  503. catch
  504. {
  505. // ignored
  506. }
  507. try
  508. {
  509. var pn = await _db.Queryable<AdoPlan>()
  510. .Where(x => x.ProductName != null && x.ProductName != "")
  511. .Select(x => x.ProductName)
  512. .Distinct()
  513. .ToListAsync();
  514. foreach (var s in pn.Where(s => !string.IsNullOrWhiteSpace(s)))
  515. products.Add(s.Trim());
  516. }
  517. catch
  518. {
  519. // ignored
  520. }
  521. return Ok(new
  522. {
  523. products = products.OrderBy(x => x, StringComparer.Ordinal).ToList(),
  524. orderNos = orderNos.OrderBy(x => x, StringComparer.Ordinal).ToList(),
  525. productionLines = lines.OrderBy(x => x, StringComparer.Ordinal).ToList()
  526. });
  527. }
  528. private sealed class HomeL1Dto
  529. {
  530. public string? ModuleCode { get; set; }
  531. public string? MetricCode { get; set; }
  532. public decimal? MetricValue { get; set; }
  533. public decimal? TargetValue { get; set; }
  534. public string? StatusColor { get; set; }
  535. public string? TrendFlag { get; set; }
  536. }
  537. private sealed class HomeL1RawDto
  538. {
  539. public string? ModuleCode { get; set; }
  540. public string? MetricCode { get; set; }
  541. public decimal? MetricValue { get; set; }
  542. public decimal? TargetValue { get; set; }
  543. public string? StatusColor { get; set; }
  544. public string? TrendFlag { get; set; }
  545. public string? Direction { get; set; }
  546. public decimal? YellowThreshold { get; set; }
  547. public decimal? RedThreshold { get; set; }
  548. }
  549. private sealed class S8AlertDto
  550. {
  551. public string? Time { get; set; }
  552. public string? Module { get; set; }
  553. public string? Message { get; set; }
  554. public string? LevelCode { get; set; }
  555. }
  556. private sealed class KpiDetailDto
  557. {
  558. public string? ModuleCode { get; set; }
  559. public string? MetricCode { get; set; }
  560. public string? MetricName { get; set; }
  561. public decimal? MetricValue { get; set; }
  562. public decimal? TargetValue { get; set; }
  563. public string? StatusColor { get; set; }
  564. public string? TrendFlag { get; set; }
  565. public DateTime? StatDate { get; set; }
  566. }
  567. private sealed class S2ScheduleDto
  568. {
  569. public string? OrderNo { get; set; }
  570. public string? Customer { get; set; }
  571. public string? Product { get; set; }
  572. public string? ProductionLine { get; set; }
  573. public decimal? Quantity { get; set; }
  574. public DateTime? DeliveryDate { get; set; }
  575. public DateTime? StartTime { get; set; }
  576. public decimal? CycleDays { get; set; }
  577. public decimal? Satisfaction { get; set; }
  578. public string? Status { get; set; }
  579. }
  580. private sealed class S2SyncStatusDto
  581. {
  582. public string? BatchId { get; set; }
  583. public string? Status { get; set; }
  584. public int? StageRows { get; set; }
  585. public int? StandardRows { get; set; }
  586. public int? DwdRows { get; set; }
  587. public DateTime? StartTime { get; set; }
  588. public DateTime? EndTime { get; set; }
  589. public string? ErrorMessage { get; set; }
  590. }
  591. private sealed class S2DecompositionDto
  592. {
  593. public string Title { get; set; } = string.Empty;
  594. public bool Active { get; set; }
  595. public List<string> Metrics { get; set; } = new();
  596. }
  597. private sealed class S2TrendDto
  598. {
  599. public string? DateLabel { get; set; }
  600. public decimal? CycleDays { get; set; }
  601. public decimal? SatisfactionPct { get; set; }
  602. }
  603. private sealed class S2DistributionDto
  604. {
  605. public string? Name { get; set; }
  606. public decimal? SatisfactionPct { get; set; }
  607. public int TotalCount { get; set; }
  608. public int RiskCount { get; set; }
  609. }
  610. private sealed class S2OperationSummaryDto
  611. {
  612. public int OperationCount { get; set; }
  613. public decimal? CompletedQty { get; set; }
  614. public decimal? ScheduledQty { get; set; }
  615. }
  616. private sealed class S2ResourceSummaryDto
  617. {
  618. public decimal? PersonCount { get; set; }
  619. public int RiskCount { get; set; }
  620. public int LineCount { get; set; }
  621. }
  622. }