AidopKanbanController.cs 53 KB

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