S9CompositeKpiWriter.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. using Microsoft.Extensions.Logging;
  2. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  3. /// <summary>
  4. /// 四个运营 L1 写入 ado_s9_kpi_value_l1_day。
  5. /// 交付周期 = 评审 + 排程 + 物料计划 + 物料上线 + 制造 + 发货,六段同日都有值才写。
  6. /// 交付满足率 = 计划发货日当天、已发数量达到订单数量的订单行 / 有订单数量的订单行。
  7. /// 生产效率 = 产出工时 / (产出工时 + 停机),休息不计入投入。无法区分停机时不写。按产线状态的工厂过滤。
  8. /// 全库存周转 = 成品周转 + 在制周转 + 物料周转,三段同日都有值才写。
  9. /// </summary>
  10. public class S9CompositeKpiWriter : ITransient
  11. {
  12. private const string ValueTable = "ado_s9_kpi_value_l1_day";
  13. private const string ModuleCode = "S9";
  14. private readonly ISqlSugarClient _db;
  15. private readonly IKpiTargetResolver _kpiTargetResolver;
  16. private readonly ILogger<S9CompositeKpiWriter> _logger;
  17. public S9CompositeKpiWriter(
  18. ISqlSugarClient db,
  19. IKpiTargetResolver kpiTargetResolver,
  20. ILogger<S9CompositeKpiWriter> logger)
  21. {
  22. _db = db;
  23. _kpiTargetResolver = kpiTargetResolver;
  24. _logger = logger;
  25. }
  26. public async Task<int> WriteRecentAsync(
  27. long tenantId, long factoryId, DateTime anchorDate, CancellationToken cancellationToken = default)
  28. {
  29. if (tenantId <= 0 || factoryId <= 0) return 0;
  30. var rows = 0;
  31. for (var offset = 13; offset >= 0; offset--)
  32. {
  33. cancellationToken.ThrowIfCancellationRequested();
  34. rows += await WriteDayAsync(tenantId, factoryId, anchorDate.Date.AddDays(-offset), cancellationToken);
  35. }
  36. return rows;
  37. }
  38. private async Task<int> WriteDayAsync(long tenantId, long factoryId, DateTime bizDate, CancellationToken cancellationToken)
  39. {
  40. try
  41. {
  42. var now = DateTime.Now;
  43. var rows = 0;
  44. var cycle = await SumSegmentsAsync(tenantId, factoryId, bizDate,
  45. ["S1_L1_001", "S2_L1_001", "S3_L1_001", "S5_L1_001", "S6_L1_001", "S7_L1_001"], 6);
  46. if (cycle.HasValue)
  47. rows += await UpsertAsync("S9_L1_002", bizDate, cycle, now, tenantId, factoryId);
  48. var fulfillment = await FulfillmentAsync(tenantId, factoryId, bizDate);
  49. if (fulfillment.HasValue)
  50. rows += await UpsertAsync("S9_L1_003", bizDate, fulfillment, now, tenantId, factoryId);
  51. var efficiency = await EfficiencyAsync(tenantId, factoryId, bizDate);
  52. if (efficiency.HasValue)
  53. rows += await UpsertAsync("S9_L1_004", bizDate, efficiency, now, tenantId, factoryId);
  54. var turnover = await SumSegmentsAsync(tenantId, factoryId, bizDate,
  55. ["S1_L1_004", "S2_L1_004", "S3_L1_004"], 3);
  56. if (turnover.HasValue)
  57. rows += await UpsertAsync("S9_L1_005", bizDate, turnover, now, tenantId, factoryId);
  58. cancellationToken.ThrowIfCancellationRequested();
  59. return rows;
  60. }
  61. catch (Exception ex) when (ex is not OperationCanceledException)
  62. {
  63. _logger.LogWarning(ex, "S9 复合指标 {Date} 写入跳过,不打断模块重建", bizDate.ToString("yyyy-MM-dd"));
  64. return 0;
  65. }
  66. }
  67. private async Task<decimal?> SumSegmentsAsync(
  68. long tenantId, long factoryId, DateTime bizDate, string[] codes, int required)
  69. {
  70. var row = await _db.Ado.SqlQueryAsync<S9SegmentSumRow>(
  71. """
  72. SELECT COUNT(*) AS N, SUM(v) AS Total
  73. FROM (
  74. SELECT metric_code, MAX(metric_value) AS v
  75. FROM ado_s9_kpi_value_l1_day
  76. WHERE tenant_id=@TenantId AND factory_id=@FactoryId AND biz_date=@BizDate
  77. AND is_deleted=0 AND metric_value IS NOT NULL
  78. AND FIND_IN_SET(metric_code, @Codes)
  79. AND module_code = SUBSTRING_INDEX(metric_code, '_', 1)
  80. GROUP BY metric_code
  81. ) s
  82. """,
  83. new { TenantId = tenantId, FactoryId = factoryId, BizDate = bizDate, Codes = string.Join(",", codes) });
  84. var hit = row.FirstOrDefault();
  85. if (hit == null || hit.N < required || hit.Total == null) return null;
  86. return Math.Round(hit.Total.Value, 4);
  87. }
  88. private async Task<decimal?> FulfillmentAsync(long tenantId, long factoryId, DateTime bizDate)
  89. {
  90. var row = await _db.Ado.SqlQueryAsync<S9RatioRow>(
  91. """
  92. SELECT
  93. SUM(CASE WHEN IFNULL(order_qty,0) > 0 AND IFNULL(delivered_qty,0) >= order_qty THEN 1 ELSE 0 END) AS Numer,
  94. SUM(CASE WHEN IFNULL(order_qty,0) > 0 THEN 1 ELSE 0 END) AS Denom
  95. FROM mdp_std_so
  96. WHERE tenant_id=@TenantId
  97. AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  98. AND deleted_flag=0
  99. AND plan_delivery_date >= @DayStart AND plan_delivery_date < @DayEnd
  100. """,
  101. new
  102. {
  103. TenantId = tenantId,
  104. FactoryId = factoryId,
  105. DayStart = bizDate.Date,
  106. DayEnd = bizDate.Date.AddDays(1)
  107. });
  108. var hit = row.FirstOrDefault();
  109. if (hit?.Denom is not > 0 || hit.Numer == null) return null;
  110. return Math.Round(hit.Numer.Value / hit.Denom.Value * 100m, 4);
  111. }
  112. /// <summary>
  113. /// 产线状态贴源里的 ProdTime / RestTime / ProdDownTime。
  114. /// 休息或停机列缺失时无法区分有效工时,不写 100%。
  115. /// LineRunRestDet 的 TransType 取值未登记,不拿它猜开工或休息。
  116. /// </summary>
  117. private async Task<decimal?> EfficiencyAsync(long tenantId, long factoryId, DateTime bizDate)
  118. {
  119. try
  120. {
  121. var row = await _db.Ado.SqlQueryAsync<S9RatioRow>(
  122. """
  123. SELECT
  124. SUM(CASE WHEN JSON_EXTRACT(raw_data,'$.ProdTime') IS NULL THEN NULL
  125. ELSE CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ProdTime')) AS DECIMAL(18,4)) END) AS Numer,
  126. SUM(CASE WHEN JSON_EXTRACT(raw_data,'$.ProdDownTime') IS NULL THEN NULL
  127. ELSE IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ProdTime')) AS DECIMAL(18,4)),0)
  128. + IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ProdDownTime')) AS DECIMAL(18,4)),0)
  129. END) AS Denom
  130. FROM mdp_stg_line_status
  131. WHERE tenant_id=@TenantId
  132. AND factory_id=@FactoryId
  133. AND DATE(COALESCE(
  134. STR_TO_DATE(LEFT(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ProdDate')),10),'%Y-%m-%d'),
  135. sync_time)) = @BizDate
  136. """,
  137. new { TenantId = tenantId, FactoryId = factoryId.ToString(), BizDate = bizDate.Date });
  138. var hit = row.FirstOrDefault();
  139. if (hit?.Numer == null || hit.Denom is not > 0) return null;
  140. return Math.Round(hit.Numer.Value / hit.Denom.Value * 100m, 4);
  141. }
  142. catch (Exception ex) when (ex is not OperationCanceledException)
  143. {
  144. _logger.LogWarning(ex, "S9_L1_004 产线运行贴源不可用,生产效率保持空");
  145. return null;
  146. }
  147. }
  148. private async Task<int> UpsertAsync(
  149. string metricCode, DateTime bizDate, decimal? metricValue, DateTime now, long tenantId, long factoryId)
  150. {
  151. bizDate = bizDate.Date;
  152. var snap = await _kpiTargetResolver.ResolveAsync(tenantId, factoryId, metricCode, ModuleCode, bizDate);
  153. var existingId = await _db.Ado.GetLongAsync(
  154. $"SELECT IFNULL((SELECT id FROM {ValueTable} WHERE tenant_id=@TenantId AND factory_id=@FactoryId " +
  155. "AND module_code=@ModuleCode AND metric_code=@MetricCode AND biz_date=@BizDate AND is_deleted=0 " +
  156. "ORDER BY id LIMIT 1), 0)",
  157. new List<SugarParameter>
  158. {
  159. new("@TenantId", tenantId),
  160. new("@FactoryId", factoryId),
  161. new("@ModuleCode", ModuleCode),
  162. new("@MetricCode", metricCode),
  163. new("@BizDate", bizDate)
  164. });
  165. if (existingId > 0)
  166. {
  167. return await _db.Ado.ExecuteCommandAsync(
  168. $"UPDATE {ValueTable} SET metric_value=@MetricValue, target_value=@TargetValue, " +
  169. "target_config_id=@TargetConfigId, target_source=@TargetSource, target_resolved_at=@TargetResolvedAt, " +
  170. "calc_time=@Now, update_time=@Now, is_deleted=0 WHERE id=@Id",
  171. new SugarParameter("@MetricValue", metricValue),
  172. new SugarParameter("@TargetValue", KpiTargetSnapshotSql.ValueOrDbNull(snap)),
  173. new SugarParameter("@TargetConfigId", KpiTargetSnapshotSql.ConfigIdOrDbNull(snap)),
  174. new SugarParameter("@TargetSource", KpiTargetSnapshotSql.SourceOrDbNull(snap)),
  175. new SugarParameter("@TargetResolvedAt", snap.ResolvedAt),
  176. new SugarParameter("@Now", now),
  177. new SugarParameter("@Id", existingId));
  178. }
  179. var nextId = Yitter.IdGenerator.YitIdHelper.NextId();
  180. return await _db.Ado.ExecuteCommandAsync($@"
  181. INSERT INTO {ValueTable}
  182. (id, tenant_id, org_id, company_id, factory_id, status, biz_date,
  183. create_time, update_time, is_deleted, is_active,
  184. module_code, metric_code, metric_value, target_value, calc_time,
  185. target_config_id, target_source, target_resolved_at)
  186. VALUES
  187. (@Id, @TenantId, NULL, NULL, @FactoryId, NULL, @BizDate,
  188. @Now, @Now, 0, 1,
  189. @ModuleCode, @MetricCode, @MetricValue, @TargetValue, @Now,
  190. @TargetConfigId, @TargetSource, @TargetResolvedAt)",
  191. new SugarParameter("@Id", nextId),
  192. new SugarParameter("@TenantId", tenantId),
  193. new SugarParameter("@FactoryId", factoryId),
  194. new SugarParameter("@BizDate", bizDate),
  195. new SugarParameter("@Now", now),
  196. new SugarParameter("@ModuleCode", ModuleCode),
  197. new SugarParameter("@MetricCode", metricCode),
  198. new SugarParameter("@MetricValue", metricValue),
  199. new SugarParameter("@TargetValue", KpiTargetSnapshotSql.ValueOrDbNull(snap)),
  200. new SugarParameter("@TargetConfigId", KpiTargetSnapshotSql.ConfigIdOrDbNull(snap)),
  201. new SugarParameter("@TargetSource", KpiTargetSnapshotSql.SourceOrDbNull(snap)),
  202. new SugarParameter("@TargetResolvedAt", snap.ResolvedAt));
  203. }
  204. }
  205. internal sealed class S9SegmentSumRow
  206. {
  207. public int N { get; set; }
  208. public decimal? Total { get; set; }
  209. }
  210. internal sealed class S9RatioRow
  211. {
  212. public decimal? Numer { get; set; }
  213. public decimal? Denom { get; set; }
  214. }