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);
}
}