using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow; namespace Admin.NET.Plugin.AiDOP.SeedData; /// /// S8-ORDER-EXECUTION-ORDER-REVIEW-SUBSTEP-PERSIST-1:ORDER_REVIEW_PLAN_CALC 阶段 L2 子节点种子。 /// 20 单 × 5 子节点 = 100 行;每单的 L2 由该单 ORDER_REVIEW_PLAN_CALC 主节点 plannedDays / actualDays 严格回卷生成: /// - L2 piHours 合计 = plannedDays × 8 /// - L2 actualHours 合计 = actualDays × 8(actualDays = null 时全部 pending) /// - 状态由 actualHours 与 piHours 比较得出 /// [IncreSeed] public class S8OrderFlowSubstepSeedData : ISqlSugarEntitySeedData { public IEnumerable HasData() => S8OrderFlowSubstepDataset.BuildSubsteps(); } internal static class S8OrderFlowSubstepDataset { internal const long SubstepIdBase = 1329909120000L; internal const string FlowCode = "ORDER_REVIEW_PLAN_CALC"; /// 1 工作日 = 8 工时(业务规则常量)。 internal const decimal HoursPerDay = 8m; /// L2 五子节点固定顺序与 PI 基线工时(plannedDays=5 时合计 40h 与主节点对齐)。 internal static readonly (string Code, string Name, decimal PiHours)[] L2Def = { ("OPINION_REVIEW", "意见评审", 8m), ("OPINION_FEEDBACK", "意见反馈", 12m), ("SECOND_REVIEW", "二次评审", 8m), ("LEADER_REVIEW", "领导意见", 10m), ("CONTRACT_SEAL", "合同盖章", 2m), }; /// L2 PI 权重(合计 1.00;用于 plannedHours 拆分)。 private static readonly decimal[] PiWeight = { 0.20m, 0.30m, 0.20m, 0.25m, 0.05m }; /// L2 延误权重(合计 1.00;当 actualHours > plannedHours 时把 delay 分配到各子节点)。 private static readonly decimal[] DelayWeight = { 0.35m, 0.25m, 0.15m, 0.20m, 0.05m }; /// L2 行回卷结果,供同批次 unit 种子复用同一份真值。 internal readonly record struct L2Row(decimal PiHours, decimal? ActualHours, string Status); public static IEnumerable BuildSubsteps() { long seq = 0; foreach (var spec in S8OrderFlowDataset.Specs) { var orderId = S8OrderFlowDataset.OrderIdBase + spec.Idx; var scenario = spec.OrderCode == "SO-2026-001" ? "PPT" : "DEMO"; var rows = BuildL2Rows(spec); for (var i = 0; i < L2Def.Length; i++) { var (code, name, _) = L2Def[i]; var row = rows[i]; yield return new AdoS8OrderFlowSubstep { Id = SubstepIdBase + (++seq), OrderId = orderId, OrderCode = spec.OrderCode, OrderFlowCode = FlowCode, SubstepCode = code, SubstepName = name, PiHours = row.PiHours, ActualHours = row.ActualHours, Status = row.Status, SortNo = i + 1, ScenarioCode = scenario, DataSource = "SEED", TenantId = 1, FactoryId = 1, CreatedAt = S8OrderFlowDataset.CreatedAt, UpdatedAt = null, IsDeleted = false, }; } } } /// /// 按订单 ORDER_REVIEW_PLAN_CALC 主节点回卷生成 5 个 L2 行。 /// 输入:spec → BuildLifecycleValues(spec)[0] = order_review 一级阶段真值。 /// 输出:5 个 L2Row,piHours/actualHours/status 严格满足合计回卷。 /// internal static L2Row[] BuildL2Rows(S8OrderFlowDataset.OrderSpec spec) { var stage = S8OrderFlowStageDataset.BuildLifecycleValues(spec)[0]; var plannedHours = stage.kpi * HoursPerDay; decimal? actualHours = stage.status == "pending" ? (decimal?)null : stage.actualDays * HoursPerDay; var piHoursArr = AllocateByWeight(plannedHours, PiWeight); if (actualHours == null) { return piHoursArr .Select(pi => new L2Row(pi, null, "pending")) .ToArray(); } decimal[] actualHoursArr; if (actualHours.Value > plannedHours) { var delay = actualHours.Value - plannedHours; var delayShare = AllocateByWeight(delay, DelayWeight); actualHoursArr = new decimal[L2Def.Length]; for (var i = 0; i < L2Def.Length; i++) actualHoursArr[i] = decimal.Round(piHoursArr[i] + delayShare[i], 1); // 末位尾差修正,保证合计严格等于 actualHours FixTail(actualHoursArr, actualHours.Value); } else if (actualHours.Value == plannedHours) { actualHoursArr = (decimal[])piHoursArr.Clone(); } else { // actualHours < plannedHours:按 PI 权重等比例压缩 actualHoursArr = AllocateByWeight(actualHours.Value, PiWeight); } return Enumerable.Range(0, L2Def.Length) .Select(i => new L2Row(piHoursArr[i], actualHoursArr[i], ClassifyByPi(actualHoursArr[i], piHoursArr[i]))) .ToArray(); } /// /// 按权重把 total 拆成与 weights 等长的 decimal 数组: /// - 前 N-1 项 = Round(total × weights[i], 1) /// - 末项 = total - 前 N-1 项之和(保留 1 位) /// 保证返回数组合计严格等于 total。 /// internal static decimal[] AllocateByWeight(decimal total, decimal[] weights) { var result = new decimal[weights.Length]; decimal headSum = 0m; for (var i = 0; i < weights.Length - 1; i++) { var v = decimal.Round(total * weights[i], 1); result[i] = v; headSum += v; } result[weights.Length - 1] = decimal.Round(total - headSum, 1); return result; } /// 把末项调整为 total - 前 N-1 项之和,吸收 Round 累积误差。 private static void FixTail(decimal[] arr, decimal total) { decimal headSum = 0m; for (var i = 0; i < arr.Length - 1; i++) headSum += arr[i]; arr[arr.Length - 1] = decimal.Round(total - headSum, 1); } /// 状态判定:≤PI=green / 超出 ≤20%=yellow / 否则 red。 internal static string ClassifyByPi(decimal actual, decimal pi) { if (pi <= 0m) return "green"; if (actual <= pi) return "green"; return (actual - pi) / pi <= 0.20m ? "yellow" : "red"; } /// 按 (orderIdx, substepIdx) 反查 substep_id(与 BuildSubsteps 的 seq 编号严格一致)。 public static long SubstepIdAt(int orderIdx, int substepIdx) { // 每单 5 个 substep,按 spec.Idx 顺序连续编号;spec.Idx 从 1 起。 return SubstepIdBase + (orderIdx - 1) * L2Def.Length + substepIdx + 1; } /// OPINION_REVIEW(sort_no=1)在 100 行 substep 中的全局 substep_id。保留以兼容现有调用。 public static long OpinionReviewSubstepId(int orderIdx) { return SubstepIdAt(orderIdx, 0); } }