S8OrderFlowSubstepSeedData.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
  2. namespace Admin.NET.Plugin.AiDOP.SeedData;
  3. /// <summary>
  4. /// S8-ORDER-EXECUTION-ORDER-REVIEW-SUBSTEP-PERSIST-1:ORDER_REVIEW_PLAN_CALC 阶段 L2 子节点种子。
  5. /// 20 单 × 5 子节点 = 100 行;每单的 L2 由该单 ORDER_REVIEW_PLAN_CALC 主节点 plannedDays / actualDays 严格回卷生成:
  6. /// - L2 piHours 合计 = plannedDays × 8
  7. /// - L2 actualHours 合计 = actualDays × 8(actualDays = null 时全部 pending)
  8. /// - 状态由 actualHours 与 piHours 比较得出
  9. /// </summary>
  10. [IncreSeed]
  11. public class S8OrderFlowSubstepSeedData : ISqlSugarEntitySeedData<AdoS8OrderFlowSubstep>
  12. {
  13. public IEnumerable<AdoS8OrderFlowSubstep> HasData() => S8OrderFlowSubstepDataset.BuildSubsteps();
  14. }
  15. internal static class S8OrderFlowSubstepDataset
  16. {
  17. internal const long SubstepIdBase = 1329909120000L;
  18. internal const string FlowCode = "ORDER_REVIEW_PLAN_CALC";
  19. /// <summary>1 工作日 = 8 工时(业务规则常量)。</summary>
  20. internal const decimal HoursPerDay = 8m;
  21. /// <summary>L2 五子节点固定顺序与 PI 基线工时(plannedDays=5 时合计 40h 与主节点对齐)。</summary>
  22. internal static readonly (string Code, string Name, decimal PiHours)[] L2Def =
  23. {
  24. ("OPINION_REVIEW", "意见评审", 8m),
  25. ("OPINION_FEEDBACK", "意见反馈", 12m),
  26. ("SECOND_REVIEW", "二次评审", 8m),
  27. ("LEADER_REVIEW", "领导意见", 10m),
  28. ("CONTRACT_SEAL", "合同盖章", 2m),
  29. };
  30. /// <summary>L2 PI 权重(合计 1.00;用于 plannedHours 拆分)。</summary>
  31. private static readonly decimal[] PiWeight = { 0.20m, 0.30m, 0.20m, 0.25m, 0.05m };
  32. /// <summary>L2 延误权重(合计 1.00;当 actualHours &gt; plannedHours 时把 delay 分配到各子节点)。</summary>
  33. private static readonly decimal[] DelayWeight = { 0.35m, 0.25m, 0.15m, 0.20m, 0.05m };
  34. /// <summary>L2 行回卷结果,供同批次 unit 种子复用同一份真值。</summary>
  35. internal readonly record struct L2Row(decimal PiHours, decimal? ActualHours, string Status);
  36. public static IEnumerable<AdoS8OrderFlowSubstep> BuildSubsteps()
  37. {
  38. long seq = 0;
  39. foreach (var spec in S8OrderFlowDataset.Specs)
  40. {
  41. var orderId = S8OrderFlowDataset.OrderIdBase + spec.Idx;
  42. var scenario = spec.OrderCode == "SO-2026-001" ? "PPT" : "DEMO";
  43. var rows = BuildL2Rows(spec);
  44. for (var i = 0; i < L2Def.Length; i++)
  45. {
  46. var (code, name, _) = L2Def[i];
  47. var row = rows[i];
  48. yield return new AdoS8OrderFlowSubstep
  49. {
  50. Id = SubstepIdBase + (++seq),
  51. OrderId = orderId,
  52. OrderCode = spec.OrderCode,
  53. OrderFlowCode = FlowCode,
  54. SubstepCode = code,
  55. SubstepName = name,
  56. PiHours = row.PiHours,
  57. ActualHours = row.ActualHours,
  58. Status = row.Status,
  59. SortNo = i + 1,
  60. ScenarioCode = scenario,
  61. DataSource = "SEED",
  62. TenantId = 1,
  63. FactoryId = 1,
  64. CreatedAt = S8OrderFlowDataset.CreatedAt,
  65. UpdatedAt = null,
  66. IsDeleted = false,
  67. };
  68. }
  69. }
  70. }
  71. /// <summary>
  72. /// 按订单 ORDER_REVIEW_PLAN_CALC 主节点回卷生成 5 个 L2 行。
  73. /// 输入:spec → BuildLifecycleValues(spec)[0] = order_review 一级阶段真值。
  74. /// 输出:5 个 L2Row,piHours/actualHours/status 严格满足合计回卷。
  75. /// </summary>
  76. internal static L2Row[] BuildL2Rows(S8OrderFlowDataset.OrderSpec spec)
  77. {
  78. var stage = S8OrderFlowStageDataset.BuildLifecycleValues(spec)[0];
  79. var plannedHours = stage.kpi * HoursPerDay;
  80. decimal? actualHours = stage.status == "pending"
  81. ? (decimal?)null
  82. : stage.actualDays * HoursPerDay;
  83. var piHoursArr = AllocateByWeight(plannedHours, PiWeight);
  84. if (actualHours == null)
  85. {
  86. return piHoursArr
  87. .Select(pi => new L2Row(pi, null, "pending"))
  88. .ToArray();
  89. }
  90. decimal[] actualHoursArr;
  91. if (actualHours.Value > plannedHours)
  92. {
  93. var delay = actualHours.Value - plannedHours;
  94. var delayShare = AllocateByWeight(delay, DelayWeight);
  95. actualHoursArr = new decimal[L2Def.Length];
  96. for (var i = 0; i < L2Def.Length; i++)
  97. actualHoursArr[i] = decimal.Round(piHoursArr[i] + delayShare[i], 1);
  98. // 末位尾差修正,保证合计严格等于 actualHours
  99. FixTail(actualHoursArr, actualHours.Value);
  100. }
  101. else if (actualHours.Value == plannedHours)
  102. {
  103. actualHoursArr = (decimal[])piHoursArr.Clone();
  104. }
  105. else
  106. {
  107. // actualHours < plannedHours:按 PI 权重等比例压缩
  108. actualHoursArr = AllocateByWeight(actualHours.Value, PiWeight);
  109. }
  110. return Enumerable.Range(0, L2Def.Length)
  111. .Select(i => new L2Row(piHoursArr[i], actualHoursArr[i], ClassifyByPi(actualHoursArr[i], piHoursArr[i])))
  112. .ToArray();
  113. }
  114. /// <summary>
  115. /// 按权重把 total 拆成与 weights 等长的 decimal 数组:
  116. /// - 前 N-1 项 = Round(total × weights[i], 1)
  117. /// - 末项 = total - 前 N-1 项之和(保留 1 位)
  118. /// 保证返回数组合计严格等于 total。
  119. /// </summary>
  120. internal static decimal[] AllocateByWeight(decimal total, decimal[] weights)
  121. {
  122. var result = new decimal[weights.Length];
  123. decimal headSum = 0m;
  124. for (var i = 0; i < weights.Length - 1; i++)
  125. {
  126. var v = decimal.Round(total * weights[i], 1);
  127. result[i] = v;
  128. headSum += v;
  129. }
  130. result[weights.Length - 1] = decimal.Round(total - headSum, 1);
  131. return result;
  132. }
  133. /// <summary>把末项调整为 total - 前 N-1 项之和,吸收 Round 累积误差。</summary>
  134. private static void FixTail(decimal[] arr, decimal total)
  135. {
  136. decimal headSum = 0m;
  137. for (var i = 0; i < arr.Length - 1; i++) headSum += arr[i];
  138. arr[arr.Length - 1] = decimal.Round(total - headSum, 1);
  139. }
  140. /// <summary>状态判定:≤PI=green / 超出 ≤20%=yellow / 否则 red。</summary>
  141. internal static string ClassifyByPi(decimal actual, decimal pi)
  142. {
  143. if (pi <= 0m) return "green";
  144. if (actual <= pi) return "green";
  145. return (actual - pi) / pi <= 0.20m ? "yellow" : "red";
  146. }
  147. /// <summary>按 (orderIdx, substepIdx) 反查 substep_id(与 BuildSubsteps 的 seq 编号严格一致)。</summary>
  148. public static long SubstepIdAt(int orderIdx, int substepIdx)
  149. {
  150. // 每单 5 个 substep,按 spec.Idx 顺序连续编号;spec.Idx 从 1 起。
  151. return SubstepIdBase + (orderIdx - 1) * L2Def.Length + substepIdx + 1;
  152. }
  153. /// <summary>OPINION_REVIEW(sort_no=1)在 100 行 substep 中的全局 substep_id。保留以兼容现有调用。</summary>
  154. public static long OpinionReviewSubstepId(int orderIdx)
  155. {
  156. return SubstepIdAt(orderIdx, 0);
  157. }
  158. }