AidopKanbanController.cs 40 KB

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