AidopKanbanController.cs 54 KB

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