AidopKanbanController.cs 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170
  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. /// 智慧运营看板基础查询下拉:产品、订单号、产线(兼容旧接口;优先 S0 主数据)。
  942. /// </summary>
  943. [HttpGet("smart-ops-filter-options")]
  944. public async Task<IActionResult> GetSmartOpsFilterOptions([FromQuery] long factoryId = 1)
  945. {
  946. _ = factoryId;
  947. var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
  948. const int cap = 200;
  949. var productItems = await SearchProductsAsync(tenantId, null, cap);
  950. var orderItems = await SearchOrderNosAsync(tenantId, null, cap);
  951. var lineItems = await SearchProductionLinesAsync(tenantId, null, cap);
  952. return Ok(new
  953. {
  954. products = productItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList(),
  955. orderNos = orderItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList(),
  956. productionLines = lineItems.Select(x => x.Value).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToList()
  957. });
  958. }
  959. private sealed class SmartOpsOptionRow
  960. {
  961. public string? Value { get; set; }
  962. }
  963. private sealed class HomeL1Dto
  964. {
  965. public string? ModuleCode { get; set; }
  966. public string? MetricCode { get; set; }
  967. public decimal? MetricValue { get; set; }
  968. public decimal? TargetValue { get; set; }
  969. public string? StatusColor { get; set; }
  970. public string? TrendFlag { get; set; }
  971. }
  972. private sealed class HomeL1RawDto
  973. {
  974. public string? ModuleCode { get; set; }
  975. public string? MetricCode { get; set; }
  976. public decimal? MetricValue { get; set; }
  977. public decimal? TargetValue { get; set; }
  978. public string? StatusColor { get; set; }
  979. public string? TrendFlag { get; set; }
  980. public string? Direction { get; set; }
  981. public decimal? YellowThreshold { get; set; }
  982. public decimal? RedThreshold { get; set; }
  983. }
  984. private sealed class S8AlertDto
  985. {
  986. public string? Time { get; set; }
  987. public string? Module { get; set; }
  988. public string? Message { get; set; }
  989. public string? LevelCode { get; set; }
  990. }
  991. private sealed class KpiDetailDto
  992. {
  993. public string? ModuleCode { get; set; }
  994. public string? MetricCode { get; set; }
  995. public string? MetricName { get; set; }
  996. public decimal? MetricValue { get; set; }
  997. public decimal? TargetValue { get; set; }
  998. public string? StatusColor { get; set; }
  999. public string? TrendFlag { get; set; }
  1000. public DateTime? StatDate { get; set; }
  1001. public string? ParentMetricCode { get; set; }
  1002. }
  1003. private sealed class KpiDrillGroupDto
  1004. {
  1005. public string? ParentMetricCode { get; set; }
  1006. public string Title { get; set; } = string.Empty;
  1007. public List<KpiDrillItemDto> Items { get; set; } = new();
  1008. }
  1009. private sealed class KpiDrillItemDto
  1010. {
  1011. public string? MetricCode { get; set; }
  1012. public string? MetricName { get; set; }
  1013. public decimal? MetricValue { get; set; }
  1014. public decimal? TargetValue { get; set; }
  1015. public string? StatusColor { get; set; }
  1016. public string? TrendFlag { get; set; }
  1017. public string? StatDate { get; set; }
  1018. }
  1019. private sealed class S2ScheduleDto
  1020. {
  1021. public string? OrderNo { get; set; }
  1022. public string? Customer { get; set; }
  1023. public string? Product { get; set; }
  1024. public string? ProductionLine { get; set; }
  1025. public decimal? Quantity { get; set; }
  1026. public DateTime? DeliveryDate { get; set; }
  1027. public DateTime? StartTime { get; set; }
  1028. public decimal? CycleDays { get; set; }
  1029. public decimal? Satisfaction { get; set; }
  1030. public string? Status { get; set; }
  1031. }
  1032. private sealed class S2SyncStatusDto
  1033. {
  1034. public string? BatchId { get; set; }
  1035. public string? Status { get; set; }
  1036. public int? StageRows { get; set; }
  1037. public int? StandardRows { get; set; }
  1038. public int? DwdRows { get; set; }
  1039. public DateTime? StartTime { get; set; }
  1040. public DateTime? EndTime { get; set; }
  1041. public string? ErrorMessage { get; set; }
  1042. }
  1043. private sealed class S2DecompositionDto
  1044. {
  1045. public string Title { get; set; } = string.Empty;
  1046. public bool Active { get; set; }
  1047. public List<string> Metrics { get; set; } = new();
  1048. }
  1049. private sealed class S2TrendDto
  1050. {
  1051. public string? DateLabel { get; set; }
  1052. public decimal? CycleDays { get; set; }
  1053. public decimal? SatisfactionPct { get; set; }
  1054. }
  1055. private sealed class S2DistributionDto
  1056. {
  1057. public string? Name { get; set; }
  1058. public decimal? SatisfactionPct { get; set; }
  1059. public int TotalCount { get; set; }
  1060. public int RiskCount { get; set; }
  1061. }
  1062. private sealed class S2OperationSummaryDto
  1063. {
  1064. public int OperationCount { get; set; }
  1065. public decimal? CompletedQty { get; set; }
  1066. public decimal? ScheduledQty { get; set; }
  1067. }
  1068. private sealed class S2ResourceSummaryDto
  1069. {
  1070. public decimal? PersonCount { get; set; }
  1071. public int RiskCount { get; set; }
  1072. public int LineCount { get; set; }
  1073. }
  1074. private sealed class S3BranchKpiGroupDto
  1075. {
  1076. public string Branch { get; set; } = string.Empty;
  1077. public string Title { get; set; } = string.Empty;
  1078. public List<S3BranchKpiDto> Items { get; set; } = new();
  1079. }
  1080. private sealed class S3BranchKpiDto
  1081. {
  1082. public string Label { get; set; } = string.Empty;
  1083. public decimal? Value { get; set; }
  1084. public string Unit { get; set; } = string.Empty;
  1085. public decimal BarPct { get; set; }
  1086. public string Status { get; set; } = "info";
  1087. }
  1088. private sealed class S3ReadinessSummaryDto
  1089. {
  1090. public int ComponentCount { get; set; }
  1091. public decimal? ReadyRatePct { get; set; }
  1092. public decimal? ShortageQty { get; set; }
  1093. }
  1094. private sealed class S3DeliverySummaryDto
  1095. {
  1096. public int SupplierCount { get; set; }
  1097. public int RiskSupplierCount { get; set; }
  1098. public decimal? RemainingQty { get; set; }
  1099. }
  1100. }