AidopKanbanController.cs 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195
  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.Order;
  5. using Admin.NET.Plugin.AiDOP.ProcurementExecution;
  6. using Admin.NET.Plugin.AiDOP.Production;
  7. using Admin.NET.Plugin.AiDOP.Supply;
  8. using Admin.NET.Plugin.AiDOP.SmartOps;
  9. using SqlSugar;
  10. namespace Admin.NET.Plugin.AiDOP.Controllers;
  11. [ApiController]
  12. [Route("api/[controller]")]
  13. [AllowAnonymous]
  14. [NonUnify]
  15. public partial class AidopKanbanController : ControllerBase
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private readonly S1MdpSyncTransformService _s1MdpSyncTransformService;
  19. private readonly S2MdpSyncTransformService _s2MdpSyncTransformService;
  20. private readonly S3MdpSyncTransformService _s3MdpSyncTransformService;
  21. private readonly S4MdpSyncTransformService _s4MdpSyncTransformService;
  22. private readonly SmartOpsKpiAtomicQueryService _atomicQuery;
  23. public AidopKanbanController(
  24. ISqlSugarClient db,
  25. S1MdpSyncTransformService s1MdpSyncTransformService,
  26. S2MdpSyncTransformService s2MdpSyncTransformService,
  27. S3MdpSyncTransformService s3MdpSyncTransformService,
  28. S4MdpSyncTransformService s4MdpSyncTransformService,
  29. SmartOpsKpiAtomicQueryService atomicQuery)
  30. {
  31. _db = db;
  32. _s1MdpSyncTransformService = s1MdpSyncTransformService;
  33. _s2MdpSyncTransformService = s2MdpSyncTransformService;
  34. _s3MdpSyncTransformService = s3MdpSyncTransformService;
  35. _s4MdpSyncTransformService = s4MdpSyncTransformService;
  36. _atomicQuery = atomicQuery;
  37. }
  38. [HttpGet("home-l1")]
  39. public async Task<IActionResult> GetHomeL1([FromQuery] long factoryId = 1)
  40. {
  41. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  42. const string sql = """
  43. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode,
  44. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  45. v.status_color AS StatusColor, v.trend_flag AS TrendFlag,
  46. k.Direction, k.YellowThreshold, k.RedThreshold
  47. FROM ado_s9_kpi_value_l1_day v
  48. LEFT JOIN ado_smart_ops_kpi_master k
  49. ON k.MetricCode = v.metric_code COLLATE utf8mb4_general_ci AND k.TenantId = v.tenant_id
  50. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.is_deleted=0
  51. AND v.biz_date = (SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
  52. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0)
  53. ORDER BY v.module_code
  54. """;
  55. var raw = await _db.Ado.SqlQueryAsync<HomeL1RawDto>(sql, new { tenantId, factoryId });
  56. var rows = raw.Select(r => new HomeL1Dto
  57. {
  58. ModuleCode = r.ModuleCode,
  59. MetricCode = r.MetricCode,
  60. MetricValue = r.MetricValue,
  61. TargetValue = r.TargetValue,
  62. TrendFlag = r.TrendFlag,
  63. StatusColor = AidopS4KpiMerge.AchievementLevel(
  64. r.MetricValue, r.TargetValue,
  65. r.Direction ?? "higher_is_better",
  66. r.YellowThreshold, r.RedThreshold)
  67. }).ToList();
  68. return Ok(rows);
  69. }
  70. [HttpGet("s8-alerts")]
  71. public async Task<IActionResult> GetS8Alerts([FromQuery] long factoryId = 1)
  72. {
  73. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  74. const string sql = """
  75. SELECT DATE_FORMAT(alert_time, '%H:%i') AS Time,
  76. IFNULL(SUBSTRING_INDEX(metric_code, '_', 1), 'S8') AS Module,
  77. IFNULL(message, '异常告警') AS Message,
  78. IFNULL(level_code, 'medium') AS LevelCode
  79. FROM ado_s8_alert_record
  80. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  81. ORDER BY alert_time DESC
  82. LIMIT 6
  83. """;
  84. var rows = await _db.Ado.SqlQueryAsync<S8AlertDto>(sql, new { tenantId, factoryId });
  85. return Ok(rows.Select(x => new
  86. {
  87. time = x.Time,
  88. module = x.Module,
  89. message = x.Message,
  90. level = x.LevelCode,
  91. levelLabel = x.LevelCode switch
  92. {
  93. "critical" => "严重",
  94. "high" => "高",
  95. "medium" => "中",
  96. _ => "一般"
  97. }
  98. }));
  99. }
  100. [HttpGet("module-detail")]
  101. public async Task<IActionResult> GetModuleDetail([FromQuery] string moduleCode = "S1", [FromQuery] long factoryId = 1)
  102. {
  103. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  104. moduleCode = string.IsNullOrWhiteSpace(moduleCode) ? "S1" : moduleCode.ToUpperInvariant();
  105. const string l2Sql = """
  106. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode,
  107. COALESCE(NULLIF(k.MetricName,''), v.metric_code) AS MetricName,
  108. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  109. v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate
  110. FROM ado_s9_kpi_value_l2_day v
  111. LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code AND k.ModuleCode=v.module_code
  112. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.is_deleted=0 AND v.module_code=@moduleCode
  113. AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l2_day
  114. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0)
  115. ORDER BY v.metric_code
  116. LIMIT 30
  117. """;
  118. const string l3Sql = """
  119. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode,
  120. COALESCE(NULLIF(k.MetricName,''), v.metric_code) AS MetricName,
  121. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  122. v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate,
  123. p.MetricCode AS ParentMetricCode
  124. FROM ado_s9_kpi_value_l3_day v
  125. LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code AND k.ModuleCode=v.module_code
  126. LEFT JOIN ado_smart_ops_kpi_master p ON p.Id=k.ParentId
  127. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.is_deleted=0 AND v.module_code=@moduleCode
  128. AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l3_day
  129. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0)
  130. ORDER BY v.metric_code
  131. LIMIT 60
  132. """;
  133. const string alertSql = """
  134. SELECT DATE_FORMAT(alert_time, '%H:%i:%s') AS Time,
  135. IFNULL(level_code, 'medium') AS LevelCode,
  136. IFNULL(message, '异常告警') AS Message
  137. FROM ado_s8_alert_record
  138. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  139. AND (UPPER(IFNULL(module_code,''))=@moduleCode OR UPPER(IFNULL(SUBSTRING_INDEX(metric_code, '_', 1), ''))=@moduleCode)
  140. ORDER BY alert_time DESC
  141. LIMIT 20
  142. """;
  143. const string l2FallbackSql = """
  144. SELECT '' AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  145. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  146. FROM ado_s9_kpi_value_l2_day
  147. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  148. ORDER BY id DESC
  149. LIMIT 30
  150. """;
  151. const string l3FallbackSql = """
  152. SELECT '' AS ModuleCode, metric_code AS MetricCode, metric_code AS MetricName, metric_value AS MetricValue,
  153. target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag, NULL AS StatDate
  154. FROM ado_s9_kpi_value_l3_day
  155. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  156. ORDER BY id DESC
  157. LIMIT 60
  158. """;
  159. var l2 = new List<KpiDetailDto>();
  160. var l3 = new List<KpiDetailDto>();
  161. var alerts = new List<S8AlertDto>();
  162. try { l2 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l2Sql, new { moduleCode, tenantId, factoryId }); }
  163. catch { l2 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l2FallbackSql, new { tenantId, factoryId }); }
  164. try { l3 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l3Sql, new { moduleCode, tenantId, factoryId }); }
  165. catch { l3 = await _db.Ado.SqlQueryAsync<KpiDetailDto>(l3FallbackSql, new { tenantId, factoryId }); }
  166. try { alerts = await _db.Ado.SqlQueryAsync<S8AlertDto>(alertSql, new { moduleCode, tenantId, factoryId }); }
  167. catch { alerts = new List<S8AlertDto>(); }
  168. var schedules = new List<S2ScheduleDto>();
  169. var s2Alerts = new List<S8AlertDto>();
  170. var decomposition = new List<S2DecompositionDto>();
  171. var trend = new List<S2TrendDto>();
  172. var distribution = new List<S2DistributionDto>();
  173. var branchKpis = new List<S3BranchKpiGroupDto>();
  174. S2SyncStatusDto? syncStatus = null;
  175. if (moduleCode == "S2")
  176. {
  177. syncStatus = await GetS2SyncStatusAsync();
  178. s2Alerts = await GetS2DerivedAlertsAsync(tenantId, factoryId);
  179. if (s2Alerts.Count > 0)
  180. alerts = s2Alerts;
  181. decomposition = await GetS2DecompositionAsync(tenantId, factoryId, l2);
  182. trend = await GetS2TrendAsync(tenantId, factoryId);
  183. distribution = await GetS2DistributionAsync(tenantId, factoryId);
  184. try
  185. {
  186. schedules = await _db.Ado.SqlQueryAsync<S2ScheduleDto>(
  187. """
  188. SELECT work_order AS OrderNo,
  189. COALESCE(NULLIF(item_name, ''), item_code, work_order) AS Product,
  190. COALESCE(NULLIF(sales_order_no, ''), work_order) AS Customer,
  191. prod_line AS ProductionLine,
  192. qty_ordered AS Quantity,
  193. due_date AS DeliveryDate,
  194. first_start_time AS StartTime,
  195. schedule_cycle_days AS CycleDays,
  196. CASE WHEN schedule_satisfaction_flag = 1 THEN 100
  197. WHEN due_date IS NULL OR last_plan_date IS NULL THEN 70
  198. ELSE 65 END AS Satisfaction,
  199. CASE WHEN schedule_satisfaction_flag = 1 THEN '已锁定'
  200. WHEN last_plan_date IS NULL THEN '待排程'
  201. ELSE '资源异常' END AS Status
  202. FROM dwd_order_schedule_trans
  203. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  204. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  205. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  206. ORDER BY calc_time DESC, id DESC LIMIT 1)
  207. ORDER BY urgent_flag DESC, due_date IS NULL, due_date, id DESC
  208. LIMIT 30
  209. """,
  210. new { tenantId, factoryId });
  211. }
  212. catch
  213. {
  214. schedules = new List<S2ScheduleDto>();
  215. }
  216. }
  217. if (moduleCode == "S3")
  218. {
  219. syncStatus = await GetMdpSyncStatusAsync("S3_MDP_SYNC_TRANSFORM");
  220. var s3Alerts = await GetS3DerivedAlertsAsync(tenantId, factoryId);
  221. if (s3Alerts.Count > 0)
  222. alerts = s3Alerts;
  223. decomposition = await GetS3DecompositionAsync(tenantId, factoryId, l2);
  224. branchKpis = await GetS3BranchKpisAsync(tenantId, factoryId, l2);
  225. trend = await GetS3TrendAsync(tenantId, factoryId);
  226. distribution = await GetS3DistributionAsync(tenantId, factoryId);
  227. }
  228. if (moduleCode == "S4")
  229. {
  230. syncStatus = await GetMdpSyncStatusAsync("S4_MDP_SYNC_TRANSFORM");
  231. var s4Alerts = await GetS4DerivedAlertsAsync(tenantId, factoryId);
  232. if (s4Alerts.Count > 0)
  233. alerts = s4Alerts;
  234. trend = await GetS4TrendAsync(tenantId, factoryId);
  235. distribution = await GetS4DistributionAsync(tenantId, factoryId);
  236. }
  237. var drillGroups = BuildKpiDrillGroups(moduleCode, l2, l3);
  238. if (moduleCode == "S2")
  239. decomposition = EnrichS2DecompositionWithL3(decomposition, l3);
  240. return Ok(new
  241. {
  242. moduleCode,
  243. l2,
  244. l3,
  245. drillGroups,
  246. syncStatus,
  247. decomposition,
  248. branchKpis,
  249. trend,
  250. distribution,
  251. schedules,
  252. alerts = alerts.Select(x => new
  253. {
  254. time = x.Time,
  255. message = x.Message,
  256. level = x.LevelCode
  257. })
  258. });
  259. }
  260. private static List<KpiDrillGroupDto> BuildKpiDrillGroups(string moduleCode, List<KpiDetailDto> l2, List<KpiDetailDto> l3)
  261. {
  262. if (l3.Count == 0) return new List<KpiDrillGroupDto>();
  263. var parentTitles = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  264. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  265. .ToDictionary(g => g.Key, g => g.First().MetricName ?? g.Key, StringComparer.OrdinalIgnoreCase);
  266. return l3.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  267. .GroupBy(u => string.IsNullOrWhiteSpace(u.ParentMetricCode) ? "_root" : u.ParentMetricCode!, StringComparer.OrdinalIgnoreCase)
  268. .Select(g =>
  269. {
  270. var parentCode = g.Key == "_root" ? null : g.Key;
  271. var title = parentCode != null && parentTitles.TryGetValue(parentCode, out var parentName)
  272. ? parentName
  273. : parentCode ?? $"{moduleCode} L3";
  274. return new KpiDrillGroupDto
  275. {
  276. ParentMetricCode = parentCode,
  277. Title = title,
  278. Items = g.Select(item => new KpiDrillItemDto
  279. {
  280. MetricCode = item.MetricCode,
  281. MetricName = item.MetricName ?? item.MetricCode,
  282. MetricValue = item.MetricValue,
  283. TargetValue = item.TargetValue,
  284. StatusColor = item.StatusColor,
  285. TrendFlag = item.TrendFlag,
  286. StatDate = item.StatDate?.ToString("yyyy-MM-dd")
  287. }).ToList()
  288. };
  289. })
  290. .OrderBy(x => x.ParentMetricCode)
  291. .ToList();
  292. }
  293. private static List<S2DecompositionDto> EnrichS2DecompositionWithL3(List<S2DecompositionDto> decomposition, List<KpiDetailDto> l3)
  294. {
  295. if (l3.Count == 0) return decomposition;
  296. var latestL3 = l3.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  297. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  298. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  299. foreach (var section in decomposition)
  300. {
  301. if (section.Title == "工序排程")
  302. {
  303. section.Metrics = new List<string>
  304. {
  305. $"1. 周期:{FormatMetric(latestL3, "S2_L3_001", "天")}",
  306. $"2. 满足率:{FormatMetric(latestL3, "S2_L3_002", "%")}",
  307. $"3. 人效:{FormatMetric(latestL3, "S2_L3_003", "道/人")}"
  308. };
  309. }
  310. else if (section.Title == "资源排程")
  311. {
  312. section.Metrics = new List<string>
  313. {
  314. $"1. 设备周期:{FormatMetric(latestL3, "S2_L3_004", "天")}",
  315. $"2. 设备满足率:{FormatMetric(latestL3, "S2_L3_005", "%")}",
  316. $"3. 设备人效:{FormatMetric(latestL3, "S2_L3_006", "台/人")}"
  317. };
  318. }
  319. }
  320. return decomposition;
  321. }
  322. [HttpPost("s1-mdp/refresh")]
  323. public async Task<IActionResult> RefreshS1Mdp(CancellationToken cancellationToken)
  324. {
  325. var result = await _s1MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
  326. return Ok(new
  327. {
  328. ok = true,
  329. result.BatchId,
  330. result.StageRows,
  331. result.StandardRows,
  332. result.DwdRows,
  333. result.KpiRows,
  334. result.AtomicRows
  335. });
  336. }
  337. [HttpPost("s2-mdp/refresh")]
  338. public async Task<IActionResult> RefreshS2Mdp(CancellationToken cancellationToken)
  339. {
  340. var result = await _s2MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
  341. return Ok(new
  342. {
  343. ok = true,
  344. result.BatchId,
  345. result.StageRows,
  346. result.StandardRows,
  347. result.DwdRows,
  348. result.KpiRows
  349. });
  350. }
  351. [HttpPost("s3-mdp/refresh")]
  352. public async Task<IActionResult> RefreshS3Mdp(CancellationToken cancellationToken)
  353. {
  354. var result = await _s3MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
  355. return Ok(new
  356. {
  357. ok = true,
  358. result.BatchId,
  359. result.StageRows,
  360. result.StandardRows,
  361. result.DwdRows,
  362. result.KpiRows
  363. });
  364. }
  365. [HttpPost("s4-mdp/refresh")]
  366. public async Task<IActionResult> RefreshS4Mdp(CancellationToken cancellationToken)
  367. {
  368. var result = await _s4MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
  369. return Ok(new
  370. {
  371. ok = true,
  372. result.BatchId,
  373. result.StageRows,
  374. result.StandardRows,
  375. result.DwdRows,
  376. result.KpiRows
  377. });
  378. }
  379. [HttpPost("mdp/refresh/{moduleCode}")]
  380. public async Task<IActionResult> RefreshModuleMdp(string moduleCode, CancellationToken cancellationToken)
  381. {
  382. var mc = string.IsNullOrWhiteSpace(moduleCode) ? string.Empty : moduleCode.Trim().ToUpperInvariant();
  383. return mc switch
  384. {
  385. "S1" => await RefreshS1Mdp(cancellationToken),
  386. "S2" => await RefreshS2Mdp(cancellationToken),
  387. "S3" => await RefreshS3Mdp(cancellationToken),
  388. "S4" => await RefreshS4Mdp(cancellationToken),
  389. _ => Ok(new { ok = true, moduleCode = mc, message = "当前模块暂无 MDP 刷新任务,仅刷新动态看板数据。" })
  390. };
  391. }
  392. private async Task<S2SyncStatusDto?> GetS2SyncStatusAsync()
  393. {
  394. return await GetMdpSyncStatusAsync("S2_MDP_SYNC_TRANSFORM");
  395. }
  396. private async Task<S2SyncStatusDto?> GetMdpSyncStatusAsync(string jobCode)
  397. {
  398. try
  399. {
  400. return await _db.Ado.SqlQuerySingleAsync<S2SyncStatusDto>(
  401. """
  402. SELECT batch_id AS BatchId, status AS Status, stage_rows AS StageRows,
  403. standard_rows AS StandardRows, dwd_rows AS DwdRows,
  404. start_time AS StartTime, end_time AS EndTime, error_message AS ErrorMessage
  405. FROM mdp_transform_run_log
  406. WHERE job_code=@JobCode
  407. ORDER BY start_time DESC, id DESC
  408. LIMIT 1
  409. """,
  410. new SugarParameter("@JobCode", jobCode));
  411. }
  412. catch
  413. {
  414. return null;
  415. }
  416. }
  417. private async Task<List<S2DecompositionDto>> GetS2DecompositionAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
  418. {
  419. var latestL1 = new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
  420. try
  421. {
  422. var rows = await _db.Ado.SqlQueryAsync<KpiDetailDto>(
  423. """
  424. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode, k.MetricName AS MetricName,
  425. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  426. v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate
  427. FROM ado_s9_kpi_value_l1_day v
  428. LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code
  429. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.module_code='S2' AND v.is_deleted=0
  430. AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
  431. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code='S2' AND is_deleted=0)
  432. """,
  433. new { tenantId, factoryId });
  434. latestL1 = rows.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  435. .ToDictionary(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase);
  436. }
  437. catch
  438. {
  439. latestL1 = new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
  440. }
  441. var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  442. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  443. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  444. var operationSummary = await GetS2OperationSummaryAsync(tenantId, factoryId);
  445. var resourceSummary = await GetS2ResourceSummaryAsync(tenantId, factoryId);
  446. return new List<S2DecompositionDto>
  447. {
  448. new()
  449. {
  450. Title = "订单排程",
  451. Active = true,
  452. Metrics = new List<string>
  453. {
  454. $"1. 周期:{FormatMetric(latestL1, "S2_L1_001", "天")}",
  455. $"2. 满足率:{FormatMetric(latestL1, "S2_L1_002", "%")}",
  456. $"3. 在制库存:{FormatMetric(latestL1, "S2_L1_004", "天")}",
  457. $"4. 人效:{FormatMetric(latestL1, "S2_L1_003", "单/人")}"
  458. }
  459. },
  460. new()
  461. {
  462. Title = "工单排程",
  463. Metrics = new List<string>
  464. {
  465. $"1. 周期:{FormatMetric(latestL2, "S2_L2_001", "天")}",
  466. $"2. 满足率:{FormatMetric(latestL2, "S2_L2_002", "%")}",
  467. $"3. 人效:{FormatMetric(latestL2, "S2_L2_003", "单/人")}"
  468. }
  469. },
  470. new()
  471. {
  472. Title = "工序排程",
  473. Metrics = new List<string>
  474. {
  475. $"1. 工序数:{operationSummary.OperationCount}",
  476. $"2. 完成量:{Math.Round(operationSummary.CompletedQty ?? 0, 2)}",
  477. $"3. 排程量:{Math.Round(operationSummary.ScheduledQty ?? 0, 2)}"
  478. }
  479. },
  480. new()
  481. {
  482. Title = "资源排程",
  483. Metrics = new List<string>
  484. {
  485. $"1. 人员数:{Math.Round(resourceSummary.PersonCount ?? 0, 2)}",
  486. $"2. 异常数:{resourceSummary.RiskCount}",
  487. $"3. 产线数:{resourceSummary.LineCount}"
  488. }
  489. }
  490. };
  491. }
  492. private async Task<S2OperationSummaryDto> GetS2OperationSummaryAsync(long tenantId, long factoryId)
  493. {
  494. try
  495. {
  496. return await _db.Ado.SqlQuerySingleAsync<S2OperationSummaryDto>(
  497. """
  498. SELECT SUM(operation_count) AS OperationCount,
  499. SUM(completed_op_qty) AS CompletedQty,
  500. SUM(scheduled_qty) AS ScheduledQty
  501. FROM dwd_order_schedule_trans
  502. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  503. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  504. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  505. ORDER BY calc_time DESC, id DESC LIMIT 1)
  506. """,
  507. new { tenantId, factoryId }) ?? new S2OperationSummaryDto();
  508. }
  509. catch
  510. {
  511. return new S2OperationSummaryDto();
  512. }
  513. }
  514. private async Task<S2ResourceSummaryDto> GetS2ResourceSummaryAsync(long tenantId, long factoryId)
  515. {
  516. try
  517. {
  518. return await _db.Ado.SqlQuerySingleAsync<S2ResourceSummaryDto>(
  519. """
  520. SELECT SUM(resource_person_count) AS PersonCount,
  521. SUM(CASE WHEN schedule_satisfaction_flag=0 THEN 1 ELSE 0 END) AS RiskCount,
  522. COUNT(DISTINCT COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配')) AS LineCount
  523. FROM dwd_order_schedule_trans
  524. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  525. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  526. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  527. ORDER BY calc_time DESC, id DESC LIMIT 1)
  528. """,
  529. new { tenantId, factoryId }) ?? new S2ResourceSummaryDto();
  530. }
  531. catch
  532. {
  533. return new S2ResourceSummaryDto();
  534. }
  535. }
  536. private async Task<List<S2TrendDto>> GetS2TrendAsync(long tenantId, long factoryId)
  537. {
  538. try
  539. {
  540. return await _db.Ado.SqlQueryAsync<S2TrendDto>(
  541. """
  542. SELECT DATE_FORMAT(d.biz_date, '%m-%d') AS DateLabel,
  543. MAX(CASE WHEN d.metric_code='S2_L1_001' THEN d.metric_value END) AS CycleDays,
  544. MAX(CASE WHEN d.metric_code='S2_L1_002' THEN d.metric_value END) AS SatisfactionPct
  545. FROM ado_s9_kpi_value_l1_day d
  546. WHERE d.tenant_id=@tenantId AND d.factory_id=@factoryId AND d.module_code='S2' AND d.is_deleted=0
  547. AND d.metric_code IN ('S2_L1_001','S2_L1_002')
  548. GROUP BY d.biz_date
  549. ORDER BY d.biz_date DESC
  550. LIMIT 7
  551. """,
  552. new { tenantId, factoryId });
  553. }
  554. catch
  555. {
  556. return new List<S2TrendDto>();
  557. }
  558. }
  559. private async Task<List<S2DistributionDto>> GetS2DistributionAsync(long tenantId, long factoryId)
  560. {
  561. try
  562. {
  563. return await _db.Ado.SqlQueryAsync<S2DistributionDto>(
  564. """
  565. SELECT COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配') AS Name,
  566. ROUND(100 * SUM(schedule_satisfaction_flag) / NULLIF(COUNT(1), 0), 2) AS SatisfactionPct,
  567. COUNT(1) AS TotalCount,
  568. SUM(CASE WHEN schedule_satisfaction_flag=0 THEN 1 ELSE 0 END) AS RiskCount
  569. FROM dwd_order_schedule_trans
  570. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  571. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  572. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  573. ORDER BY calc_time DESC, id DESC LIMIT 1)
  574. GROUP BY COALESCE(NULLIF(prod_line,''), NULLIF(site_code,''), '未分配')
  575. ORDER BY SatisfactionPct, TotalCount DESC
  576. LIMIT 8
  577. """,
  578. new { tenantId, factoryId });
  579. }
  580. catch
  581. {
  582. return new List<S2DistributionDto>();
  583. }
  584. }
  585. private async Task<List<S8AlertDto>> GetS2DerivedAlertsAsync(long tenantId, long factoryId)
  586. {
  587. try
  588. {
  589. return await _db.Ado.SqlQueryAsync<S8AlertDto>(
  590. """
  591. SELECT DATE_FORMAT(calc_time, '%H:%i:%s') AS Time,
  592. CASE
  593. WHEN last_plan_date IS NULL THEN 'high'
  594. WHEN due_date IS NOT NULL AND last_plan_date > due_date THEN 'critical'
  595. WHEN IFNULL(resource_person_count, 0) <= 0 THEN 'medium'
  596. ELSE 'info'
  597. END AS LevelCode,
  598. CONCAT('工单 ', work_order, ' ',
  599. CASE
  600. WHEN last_plan_date IS NULL THEN '尚未形成排程'
  601. WHEN due_date IS NOT NULL AND last_plan_date > due_date THEN '排程晚于交期'
  602. WHEN IFNULL(resource_person_count, 0) <= 0 THEN '缺少资源人员配置'
  603. ELSE '排程状态正常'
  604. END) AS Message
  605. FROM dwd_order_schedule_trans
  606. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  607. AND calc_batch_id=(SELECT calc_batch_id FROM dwd_order_schedule_trans
  608. WHERE tenant_id=@tenantId AND factory_id=@factoryId
  609. ORDER BY calc_time DESC, id DESC LIMIT 1)
  610. 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)
  611. ORDER BY FIELD(LevelCode, 'critical', 'high', 'medium', 'info'), due_date IS NULL, due_date
  612. LIMIT 20
  613. """,
  614. new { tenantId, factoryId });
  615. }
  616. catch
  617. {
  618. return new List<S8AlertDto>();
  619. }
  620. }
  621. private async Task<List<S2DecompositionDto>> GetS3DecompositionAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
  622. {
  623. var l1 = await GetLatestModuleL1Async("S3", tenantId, factoryId);
  624. var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  625. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  626. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  627. var readiness = await GetS3ReadinessSummaryAsync(tenantId);
  628. var delivery = await GetS3DeliverySummaryAsync(tenantId);
  629. return new List<S2DecompositionDto>
  630. {
  631. new()
  632. {
  633. Title = "物料需求计划",
  634. Active = true,
  635. Metrics = new List<string>
  636. {
  637. $"1. 周期:{FormatMetric(l1, "S3_L1_001", "天")}",
  638. $"2. 满足率:{Math.Round(readiness.ReadyRatePct ?? GetMetricValue(l1, "S3_L1_002") ?? 0, 2)}%",
  639. $"3. 覆盖SKU:{readiness.ComponentCount}",
  640. $"4. 缺口数量:{Math.Round(readiness.ShortageQty ?? 0, 2)}"
  641. }
  642. },
  643. new()
  644. {
  645. Title = "物料交货计划",
  646. Metrics = new List<string>
  647. {
  648. $"1. 周期:{FormatMetric(latestL2, "S3_L2_004", "天")}",
  649. $"2. 满足率:{FormatMetric(latestL2, "S3_L2_005", "%")}",
  650. $"3. 风险供应商:{delivery.RiskSupplierCount}",
  651. $"4. 待交数量:{Math.Round(delivery.RemainingQty ?? 0, 2)}"
  652. }
  653. }
  654. };
  655. }
  656. private async Task<List<S3BranchKpiGroupDto>> GetS3BranchKpisAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
  657. {
  658. var l1 = await GetLatestModuleL1Async("S3", tenantId, factoryId);
  659. var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  660. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  661. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  662. var readiness = await GetS3ReadinessSummaryAsync(tenantId);
  663. var delivery = await GetS3DeliverySummaryAsync(tenantId);
  664. var demandCycle = GetMetricValue(l1, "S3_L1_001");
  665. var demandReadyRate = readiness.ReadyRatePct ?? GetMetricValue(l1, "S3_L1_002");
  666. var deliveryCycle = GetMetricValue(latestL2, "S3_L2_004");
  667. var deliveryRate = GetMetricValue(latestL2, "S3_L2_005");
  668. return new List<S3BranchKpiGroupDto>
  669. {
  670. new()
  671. {
  672. Branch = "MRP",
  673. Title = "物料需求计划",
  674. Items = new List<S3BranchKpiDto>
  675. {
  676. BuildBranchKpi("需求计划周期", demandCycle, "d", InverseBar(demandCycle, 15m)),
  677. BuildBranchKpi("需求计划满足率", demandReadyRate, "%", PercentBar(demandReadyRate)),
  678. BuildBranchKpi("计划覆盖SKU", readiness.ComponentCount, "SKU", PercentBar(readiness.ComponentCount, Math.Max(readiness.ComponentCount, 1)))
  679. }
  680. },
  681. new()
  682. {
  683. Branch = "MDP",
  684. Title = "物料交货计划",
  685. Items = new List<S3BranchKpiDto>
  686. {
  687. BuildBranchKpi("交货计划周期", deliveryCycle, "d", InverseBar(deliveryCycle, 10.5m)),
  688. BuildBranchKpi("交货计划满足率", deliveryRate, "%", PercentBar(deliveryRate)),
  689. BuildBranchKpi("风险供应商", delivery.RiskSupplierCount, "家", InverseBar(delivery.RiskSupplierCount, Math.Max(delivery.SupplierCount, 1)))
  690. }
  691. }
  692. };
  693. }
  694. private async Task<List<S2TrendDto>> GetS3TrendAsync(long tenantId, long factoryId)
  695. {
  696. try
  697. {
  698. return await _db.Ado.SqlQueryAsync<S2TrendDto>(
  699. """
  700. SELECT DATE_FORMAT(d.biz_date, '%m-%d') AS DateLabel,
  701. MAX(CASE WHEN d.metric_code='S3_L1_001' THEN d.metric_value END) AS CycleDays,
  702. MAX(CASE WHEN d.metric_code='S3_L1_002' THEN d.metric_value END) AS SatisfactionPct
  703. FROM ado_s9_kpi_value_l1_day d
  704. WHERE d.tenant_id=@tenantId AND d.factory_id=@factoryId AND d.module_code='S3' AND d.is_deleted=0
  705. AND d.metric_code IN ('S3_L1_001','S3_L1_002')
  706. GROUP BY d.biz_date
  707. ORDER BY d.biz_date DESC
  708. LIMIT 7
  709. """,
  710. new { tenantId, factoryId });
  711. }
  712. catch
  713. {
  714. return new List<S2TrendDto>();
  715. }
  716. }
  717. private async Task<List<S2DistributionDto>> GetS3DistributionAsync(long tenantId, long factoryId)
  718. {
  719. _ = factoryId;
  720. try
  721. {
  722. return await _db.Ado.SqlQueryAsync<S2DistributionDto>(
  723. """
  724. SELECT COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配') AS Name,
  725. ROUND(100 * SUM(CASE WHEN IFNULL(schedule_qty,0) >= IFNULL(order_qty,0) AND IFNULL(order_qty,0) > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(1), 0), 2) AS SatisfactionPct,
  726. COUNT(1) AS TotalCount,
  727. SUM(CASE WHEN risk_level IN ('HIGH','MEDIUM') THEN 1 ELSE 0 END) AS RiskCount
  728. FROM dwd_supplier_delivery
  729. WHERE tenant_id=@tenantId
  730. AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
  731. GROUP BY COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配')
  732. ORDER BY RiskCount DESC, SatisfactionPct, TotalCount DESC
  733. LIMIT 8
  734. """,
  735. new { tenantId });
  736. }
  737. catch
  738. {
  739. return new List<S2DistributionDto>();
  740. }
  741. }
  742. private async Task<List<S8AlertDto>> GetS3DerivedAlertsAsync(long tenantId, long factoryId)
  743. {
  744. _ = factoryId;
  745. try
  746. {
  747. return await _db.Ado.SqlQueryAsync<S8AlertDto>(
  748. """
  749. SELECT DATE_FORMAT(calc_time, '%H:%i:%s') AS Time,
  750. CASE WHEN risk_level='HIGH' THEN 'critical'
  751. WHEN risk_level='MEDIUM' THEN 'warning'
  752. ELSE 'info' END AS LevelCode,
  753. CONCAT('供应商 ', COALESCE(NULLIF(supplier_name,''), supplier_code, '未分配'),
  754. ' 物料 ', COALESCE(NULLIF(item_code,''), '未指定'),
  755. ':', COALESCE(NULLIF(risk_reason,''), risk_type, '供应风险')) AS Message
  756. FROM dwd_supplier_risk
  757. WHERE tenant_id=@tenantId
  758. AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_risk WHERE tenant_id=@tenantId)
  759. ORDER BY FIELD(risk_level, 'HIGH', 'MEDIUM', 'LOW'), risk_count DESC, calc_time DESC
  760. LIMIT 20
  761. """,
  762. new { tenantId });
  763. }
  764. catch
  765. {
  766. return new List<S8AlertDto>();
  767. }
  768. }
  769. private async Task<List<S2TrendDto>> GetS4TrendAsync(long tenantId, long factoryId)
  770. {
  771. try
  772. {
  773. return await _db.Ado.SqlQueryAsync<S2TrendDto>(
  774. """
  775. SELECT DATE_FORMAT(d.biz_date, '%m-%d') AS DateLabel,
  776. MAX(CASE WHEN d.metric_code='S4_L1_001' THEN d.metric_value END) AS CycleDays,
  777. MAX(CASE WHEN d.metric_code='S4_L1_002' THEN d.metric_value END) AS SatisfactionPct
  778. FROM ado_s9_kpi_value_l1_day d
  779. WHERE d.tenant_id=@tenantId AND d.factory_id=@factoryId AND d.module_code='S4' AND d.is_deleted=0
  780. AND d.metric_code IN ('S4_L1_001','S4_L1_002')
  781. GROUP BY d.biz_date
  782. ORDER BY d.biz_date DESC
  783. LIMIT 7
  784. """,
  785. new { tenantId, factoryId });
  786. }
  787. catch
  788. {
  789. return new List<S2TrendDto>();
  790. }
  791. }
  792. private async Task<List<S2DistributionDto>> GetS4DistributionAsync(long tenantId, long factoryId)
  793. {
  794. _ = factoryId;
  795. try
  796. {
  797. return await _db.Ado.SqlQueryAsync<S2DistributionDto>(
  798. """
  799. SELECT COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配') AS Name,
  800. ROUND(100 * SUM(CASE WHEN IFNULL(receipt_qty,0) >= IFNULL(order_qty,0) AND IFNULL(order_qty,0) > 0 THEN 1
  801. WHEN delivery_status='COMPLETED' THEN 1 ELSE 0 END) / NULLIF(COUNT(1), 0), 2) AS SatisfactionPct,
  802. COUNT(1) AS TotalCount,
  803. SUM(CASE WHEN risk_level IN ('HIGH','MEDIUM') THEN 1 ELSE 0 END) AS RiskCount
  804. FROM dwd_supplier_delivery
  805. WHERE tenant_id=@tenantId
  806. AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
  807. GROUP BY COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配')
  808. ORDER BY RiskCount DESC, SatisfactionPct, TotalCount DESC
  809. LIMIT 8
  810. """,
  811. new { tenantId });
  812. }
  813. catch
  814. {
  815. return new List<S2DistributionDto>();
  816. }
  817. }
  818. private async Task<List<S8AlertDto>> GetS4DerivedAlertsAsync(long tenantId, long factoryId)
  819. {
  820. _ = factoryId;
  821. try
  822. {
  823. var fromDwd = await _db.Ado.SqlQueryAsync<S8AlertDto>(
  824. """
  825. SELECT DATE_FORMAT(calc_time, '%H:%i:%s') AS Time,
  826. CASE WHEN risk_level='HIGH' THEN 'critical'
  827. WHEN risk_level='MEDIUM' THEN 'warning'
  828. ELSE 'info' END AS LevelCode,
  829. CONCAT('采购订单 ', COALESCE(po_no, '—'), ' 供应商 ', COALESCE(NULLIF(supplier_name,''), supplier_code, '未分配'),
  830. ':', COALESCE(delivery_status, 'OPEN'), ' 剩余 ', CAST(IFNULL(remaining_qty,0) AS CHAR)) AS Message
  831. FROM dwd_supplier_delivery
  832. WHERE tenant_id=@tenantId
  833. AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
  834. AND risk_level IN ('HIGH','MEDIUM')
  835. ORDER BY FIELD(risk_level, 'HIGH', 'MEDIUM'), remaining_qty DESC
  836. LIMIT 15
  837. """,
  838. new { tenantId });
  839. if (fromDwd.Count > 0)
  840. return fromDwd;
  841. }
  842. catch
  843. {
  844. // fall through to S8 alerts
  845. }
  846. try
  847. {
  848. return await _db.Ado.SqlQueryAsync<S8AlertDto>(
  849. """
  850. SELECT DATE_FORMAT(alert_time, '%H:%i:%s') AS Time,
  851. IFNULL(level_code, 'medium') AS LevelCode,
  852. IFNULL(message, '采购执行异常') AS Message
  853. FROM ado_s8_alert_record
  854. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND is_deleted=0
  855. AND (UPPER(IFNULL(module_code,''))='S4' OR metric_code LIKE 'S4%')
  856. ORDER BY alert_time DESC
  857. LIMIT 20
  858. """,
  859. new { tenantId, factoryId });
  860. }
  861. catch
  862. {
  863. return new List<S8AlertDto>();
  864. }
  865. }
  866. private async Task<Dictionary<string, KpiDetailDto>> GetLatestModuleL1Async(string moduleCode, long tenantId, long factoryId)
  867. {
  868. try
  869. {
  870. var rows = await _db.Ado.SqlQueryAsync<KpiDetailDto>(
  871. """
  872. SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode, k.MetricName AS MetricName,
  873. v.metric_value AS MetricValue, v.target_value AS TargetValue,
  874. v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate
  875. FROM ado_s9_kpi_value_l1_day v
  876. LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code
  877. WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.module_code=@moduleCode AND v.is_deleted=0
  878. AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
  879. WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0)
  880. """,
  881. new { moduleCode, tenantId, factoryId });
  882. return rows.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
  883. .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
  884. .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
  885. }
  886. catch
  887. {
  888. return new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
  889. }
  890. }
  891. private async Task<S3ReadinessSummaryDto> GetS3ReadinessSummaryAsync(long tenantId)
  892. {
  893. try
  894. {
  895. return await _db.Ado.SqlQuerySingleAsync<S3ReadinessSummaryDto>(
  896. """
  897. SELECT COUNT(DISTINCT component_item_code) AS ComponentCount,
  898. ROUND(100 * SUM(CASE WHEN IFNULL(shortage_qty,0) <= 0 OR UPPER(IFNULL(ready_status,'')) IN ('READY','SUFFICIENT','OK') THEN 1 ELSE 0 END) / NULLIF(COUNT(1), 0), 2) AS ReadyRatePct,
  899. SUM(IFNULL(shortage_qty, 0)) AS ShortageQty
  900. FROM dwd_material_readiness
  901. WHERE tenant_id=@tenantId
  902. AND stat_date=(SELECT MAX(stat_date) FROM dwd_material_readiness WHERE tenant_id=@tenantId)
  903. """,
  904. new { tenantId }) ?? new S3ReadinessSummaryDto();
  905. }
  906. catch
  907. {
  908. return new S3ReadinessSummaryDto();
  909. }
  910. }
  911. private async Task<S3DeliverySummaryDto> GetS3DeliverySummaryAsync(long tenantId)
  912. {
  913. try
  914. {
  915. return await _db.Ado.SqlQuerySingleAsync<S3DeliverySummaryDto>(
  916. """
  917. SELECT COUNT(DISTINCT supplier_code) AS SupplierCount,
  918. COUNT(DISTINCT CASE WHEN risk_level IN ('HIGH','MEDIUM') THEN supplier_code END) AS RiskSupplierCount,
  919. SUM(IFNULL(remaining_qty, 0)) AS RemainingQty
  920. FROM dwd_supplier_delivery
  921. WHERE tenant_id=@tenantId
  922. AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
  923. """,
  924. new { tenantId }) ?? new S3DeliverySummaryDto();
  925. }
  926. catch
  927. {
  928. return new S3DeliverySummaryDto();
  929. }
  930. }
  931. private static string FormatMetric(Dictionary<string, KpiDetailDto> rows, string metricCode, string unit)
  932. {
  933. if (!rows.TryGetValue(metricCode, out var row) || row.MetricValue == null)
  934. return $"--{unit}";
  935. return $"{Math.Round(row.MetricValue.Value, 2)}{unit}";
  936. }
  937. private static decimal? GetMetricValue(Dictionary<string, KpiDetailDto> rows, string metricCode)
  938. {
  939. return rows.TryGetValue(metricCode, out var row) ? row.MetricValue : null;
  940. }
  941. private static S3BranchKpiDto BuildBranchKpi(string label, decimal? value, string unit, decimal barPct)
  942. {
  943. return new S3BranchKpiDto
  944. {
  945. Label = label,
  946. Value = value,
  947. Unit = unit,
  948. BarPct = barPct,
  949. Status = barPct >= 90 ? "success" : barPct >= 70 ? "warning" : "danger"
  950. };
  951. }
  952. private static decimal PercentBar(decimal? value, decimal max = 100m)
  953. {
  954. if (value == null || max <= 0)
  955. return 0;
  956. return Math.Round(Math.Max(0, Math.Min(100, value.Value / max * 100)), 2);
  957. }
  958. private static decimal InverseBar(decimal? value, decimal target)
  959. {
  960. if (value == null || value <= 0 || target <= 0)
  961. return 0;
  962. return Math.Round(Math.Max(0, Math.Min(100, target / value.Value * 100)), 2);
  963. }
  964. /// <summary>
  965. /// 智慧运营看板基础查询下拉:产品、订单号、产线(兼容旧接口;优先 S0 主数据)。
  966. /// </summary>
  967. [HttpGet("smart-ops-filter-options")]
  968. public async Task<IActionResult> GetSmartOpsFilterOptions([FromQuery] long factoryId = 1)
  969. {
  970. _ = factoryId;
  971. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  972. const int cap = 200;
  973. var productItems = await SearchProductsAsync(tenantId, null, cap);
  974. var orderItems = await SearchOrderNosAsync(tenantId, null, cap);
  975. var lineItems = await SearchProductionLinesAsync(tenantId, null, cap);
  976. return Ok(new
  977. {
  978. products = productItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList(),
  979. orderNos = orderItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList(),
  980. productionLines = lineItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList()
  981. });
  982. }
  983. private sealed class SmartOpsOptionRow
  984. {
  985. public string? Value { get; set; }
  986. }
  987. private sealed class HomeL1Dto
  988. {
  989. public string? ModuleCode { get; set; }
  990. public string? MetricCode { get; set; }
  991. public decimal? MetricValue { get; set; }
  992. public decimal? TargetValue { get; set; }
  993. public string? StatusColor { get; set; }
  994. public string? TrendFlag { get; set; }
  995. }
  996. private sealed class HomeL1RawDto
  997. {
  998. public string? ModuleCode { get; set; }
  999. public string? MetricCode { get; set; }
  1000. public decimal? MetricValue { get; set; }
  1001. public decimal? TargetValue { get; set; }
  1002. public string? StatusColor { get; set; }
  1003. public string? TrendFlag { get; set; }
  1004. public string? Direction { get; set; }
  1005. public decimal? YellowThreshold { get; set; }
  1006. public decimal? RedThreshold { get; set; }
  1007. }
  1008. private sealed class S8AlertDto
  1009. {
  1010. public string? Time { get; set; }
  1011. public string? Module { get; set; }
  1012. public string? Message { get; set; }
  1013. public string? LevelCode { get; set; }
  1014. }
  1015. private sealed class KpiDetailDto
  1016. {
  1017. public string? ModuleCode { get; set; }
  1018. public string? MetricCode { get; set; }
  1019. public string? MetricName { get; set; }
  1020. public decimal? MetricValue { get; set; }
  1021. public decimal? TargetValue { get; set; }
  1022. public string? StatusColor { get; set; }
  1023. public string? TrendFlag { get; set; }
  1024. public DateTime? StatDate { get; set; }
  1025. public string? ParentMetricCode { get; set; }
  1026. }
  1027. private sealed class KpiDrillGroupDto
  1028. {
  1029. public string? ParentMetricCode { get; set; }
  1030. public string Title { get; set; } = string.Empty;
  1031. public List<KpiDrillItemDto> Items { get; set; } = new();
  1032. }
  1033. private sealed class KpiDrillItemDto
  1034. {
  1035. public string? MetricCode { get; set; }
  1036. public string? MetricName { get; set; }
  1037. public decimal? MetricValue { get; set; }
  1038. public decimal? TargetValue { get; set; }
  1039. public string? StatusColor { get; set; }
  1040. public string? TrendFlag { get; set; }
  1041. public string? StatDate { get; set; }
  1042. }
  1043. private sealed class S2ScheduleDto
  1044. {
  1045. public string? OrderNo { get; set; }
  1046. public string? Customer { get; set; }
  1047. public string? Product { get; set; }
  1048. public string? ProductionLine { get; set; }
  1049. public decimal? Quantity { get; set; }
  1050. public DateTime? DeliveryDate { get; set; }
  1051. public DateTime? StartTime { get; set; }
  1052. public decimal? CycleDays { get; set; }
  1053. public decimal? Satisfaction { get; set; }
  1054. public string? Status { get; set; }
  1055. }
  1056. private sealed class S2SyncStatusDto
  1057. {
  1058. public string? BatchId { get; set; }
  1059. public string? Status { get; set; }
  1060. public int? StageRows { get; set; }
  1061. public int? StandardRows { get; set; }
  1062. public int? DwdRows { get; set; }
  1063. public DateTime? StartTime { get; set; }
  1064. public DateTime? EndTime { get; set; }
  1065. public string? ErrorMessage { get; set; }
  1066. }
  1067. private sealed class S2DecompositionDto
  1068. {
  1069. public string Title { get; set; } = string.Empty;
  1070. public bool Active { get; set; }
  1071. public List<string> Metrics { get; set; } = new();
  1072. }
  1073. private sealed class S2TrendDto
  1074. {
  1075. public string? DateLabel { get; set; }
  1076. public decimal? CycleDays { get; set; }
  1077. public decimal? SatisfactionPct { get; set; }
  1078. }
  1079. private sealed class S2DistributionDto
  1080. {
  1081. public string? Name { get; set; }
  1082. public decimal? SatisfactionPct { get; set; }
  1083. public int TotalCount { get; set; }
  1084. public int RiskCount { get; set; }
  1085. }
  1086. private sealed class S2OperationSummaryDto
  1087. {
  1088. public int OperationCount { get; set; }
  1089. public decimal? CompletedQty { get; set; }
  1090. public decimal? ScheduledQty { get; set; }
  1091. }
  1092. private sealed class S2ResourceSummaryDto
  1093. {
  1094. public decimal? PersonCount { get; set; }
  1095. public int RiskCount { get; set; }
  1096. public int LineCount { get; set; }
  1097. }
  1098. private sealed class S3BranchKpiGroupDto
  1099. {
  1100. public string Branch { get; set; } = string.Empty;
  1101. public string Title { get; set; } = string.Empty;
  1102. public List<S3BranchKpiDto> Items { get; set; } = new();
  1103. }
  1104. private sealed class S3BranchKpiDto
  1105. {
  1106. public string Label { get; set; } = string.Empty;
  1107. public decimal? Value { get; set; }
  1108. public string Unit { get; set; } = string.Empty;
  1109. public decimal BarPct { get; set; }
  1110. public string Status { get; set; } = "info";
  1111. }
  1112. private sealed class S3ReadinessSummaryDto
  1113. {
  1114. public int ComponentCount { get; set; }
  1115. public decimal? ReadyRatePct { get; set; }
  1116. public decimal? ShortageQty { get; set; }
  1117. }
  1118. private sealed class S3DeliverySummaryDto
  1119. {
  1120. public int SupplierCount { get; set; }
  1121. public int RiskSupplierCount { get; set; }
  1122. public decimal? RemainingQty { get; set; }
  1123. }
  1124. }