| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
- namespace Admin.NET.Plugin.AiDOP.SeedData;
- /// <summary>
- /// 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 比较得出
- /// </summary>
- [IncreSeed]
- public class S8OrderFlowSubstepSeedData : ISqlSugarEntitySeedData<AdoS8OrderFlowSubstep>
- {
- public IEnumerable<AdoS8OrderFlowSubstep> HasData() => S8OrderFlowSubstepDataset.BuildSubsteps();
- }
- internal static class S8OrderFlowSubstepDataset
- {
- internal const long SubstepIdBase = 1329909120000L;
- internal const string FlowCode = "ORDER_REVIEW_PLAN_CALC";
- /// <summary>1 工作日 = 8 工时(业务规则常量)。</summary>
- internal const decimal HoursPerDay = 8m;
- /// <summary>L2 五子节点固定顺序与 PI 基线工时(plannedDays=5 时合计 40h 与主节点对齐)。</summary>
- 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),
- };
- /// <summary>L2 PI 权重(合计 1.00;用于 plannedHours 拆分)。</summary>
- private static readonly decimal[] PiWeight = { 0.20m, 0.30m, 0.20m, 0.25m, 0.05m };
- /// <summary>L2 延误权重(合计 1.00;当 actualHours > plannedHours 时把 delay 分配到各子节点)。</summary>
- private static readonly decimal[] DelayWeight = { 0.35m, 0.25m, 0.15m, 0.20m, 0.05m };
- /// <summary>L2 行回卷结果,供同批次 unit 种子复用同一份真值。</summary>
- internal readonly record struct L2Row(decimal PiHours, decimal? ActualHours, string Status);
- public static IEnumerable<AdoS8OrderFlowSubstep> 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,
- };
- }
- }
- }
- /// <summary>
- /// 按订单 ORDER_REVIEW_PLAN_CALC 主节点回卷生成 5 个 L2 行。
- /// 输入:spec → BuildLifecycleValues(spec)[0] = order_review 一级阶段真值。
- /// 输出:5 个 L2Row,piHours/actualHours/status 严格满足合计回卷。
- /// </summary>
- 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();
- }
- /// <summary>
- /// 按权重把 total 拆成与 weights 等长的 decimal 数组:
- /// - 前 N-1 项 = Round(total × weights[i], 1)
- /// - 末项 = total - 前 N-1 项之和(保留 1 位)
- /// 保证返回数组合计严格等于 total。
- /// </summary>
- 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;
- }
- /// <summary>把末项调整为 total - 前 N-1 项之和,吸收 Round 累积误差。</summary>
- 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);
- }
- /// <summary>状态判定:≤PI=green / 超出 ≤20%=yellow / 否则 red。</summary>
- 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";
- }
- /// <summary>按 (orderIdx, substepIdx) 反查 substep_id(与 BuildSubsteps 的 seq 编号严格一致)。</summary>
- public static long SubstepIdAt(int orderIdx, int substepIdx)
- {
- // 每单 5 个 substep,按 spec.Idx 顺序连续编号;spec.Idx 从 1 起。
- return SubstepIdBase + (orderIdx - 1) * L2Def.Length + substepIdx + 1;
- }
- /// <summary>OPINION_REVIEW(sort_no=1)在 100 行 substep 中的全局 substep_id。保留以兼容现有调用。</summary>
- public static long OpinionReviewSubstepId(int orderIdx)
- {
- return SubstepIdAt(orderIdx, 0);
- }
- }
|