S8OrderFlowSubstepSeedData.cs 7.4 KB

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