AidopKanbanController.cs 55 KB

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