369 lines
15 KiB
C#
369 lines
15 KiB
C#
using DongfangHydro.Dashboard.Api.Domain;
|
||
using DongfangHydro.Dashboard.Api.Services;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace DongfangHydro.Dashboard.Api.Data;
|
||
|
||
public sealed class DashboardSeeder(
|
||
DashboardDbContext dbContext,
|
||
ProgressAggregationService aggregationService)
|
||
{
|
||
private static readonly string[] ProcessNames = ["备料齐套", "生产装配", "检验入库"];
|
||
|
||
private static readonly OrderBlueprint[] Orders =
|
||
[
|
||
new("MO-260708-001", "青峪口尾水单向门机 2×160/10", 1200, "总装一线", "周敏", 68, 4.3, "industrial-pc"),
|
||
new("MO-260708-002", "电站尾水2×160kN/100kN单向门式启闭机", 860, "总装二线", "李澈", 82, 2.8, "workstation"),
|
||
new("MO-260708-003", "主起升机构", 420, "高配小批线", "陈嘉", 54, -1.2, "workstation"),
|
||
new("MO-260708-004", "100kN回转吊", 1500, "总装三线", "罗岚", 43, -3.1, "aio"),
|
||
new("MO-260708-005", "门架", 2100, "柔性装配线", "宋一", 76, 1.6, "industrial-pc"),
|
||
new("MO-260708-006", "大车行走机构", 640, "精密装配线", "韩策", 61, -0.8, "edge-box"),
|
||
];
|
||
|
||
private static readonly PartBlueprint[] Parts =
|
||
[
|
||
new("主起升机构", "self_made", "重装车间", "自产工段", "fixture",
|
||
[
|
||
new("卷扬机构", "self_made", "起升装配组", "自产工段", "chassis"),
|
||
new("滑轮组", "outsourced", "机加协作组", "东方重工协作厂", "thermal"),
|
||
new("制动装置", "purchased", "采购一组", "华东液压", "power"),
|
||
]),
|
||
new("100kN回转吊", "outsourced", "回转装配组", "西南重装", "chassis",
|
||
[
|
||
new("回转支承", "purchased", "采购二组", "洛轴供应链", "chassis"),
|
||
new("起升卷筒", "self_made", "卷筒制造组", "自产工段", "fixture"),
|
||
new("吊钩组", "outsourced", "外协质检组", "川重锻造", "generic"),
|
||
]),
|
||
new("门架", "self_made", "结构车间", "自产工段", "mainboard",
|
||
[
|
||
new("主梁结构", "self_made", "铆焊一组", "自产工段", "mainboard"),
|
||
new("支腿结构", "self_made", "铆焊二组", "自产工段", "mainboard"),
|
||
new("连接平台", "outsourced", "结构外协组", "青峪钢构", "generic"),
|
||
]),
|
||
new("大车行走机构", "self_made", "行走机构组", "自产工段", "chassis",
|
||
[
|
||
new("行走台车", "self_made", "台车装配组", "自产工段", "chassis"),
|
||
new("轨道夹持器", "purchased", "采购三组", "水工制动", "fixture"),
|
||
new("驱动减速机", "purchased", "传动采购组", "国机传动", "power"),
|
||
]),
|
||
new("门机电气控制系统", "purchased", "电气车间", "东方电控", "power",
|
||
[
|
||
new("PLC控制柜", "self_made", "电控装配组", "自产工段", "power"),
|
||
new("电缆卷筒", "outsourced", "电缆协作组", "长江电缆", "cable"),
|
||
new("限位与监测装置", "purchased", "仪控采购组", "水工智控", "chip"),
|
||
]),
|
||
];
|
||
|
||
public async Task SeedAsync(CancellationToken cancellationToken)
|
||
{
|
||
if (await dbContext.ProductionOrders.AnyAsync(cancellationToken))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var lines = Orders
|
||
.Select((order, index) => new ProductionLine
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Code = $"LINE-{index + 1:00}",
|
||
Name = order.LineName,
|
||
Owner = order.Owner,
|
||
})
|
||
.ToList();
|
||
dbContext.ProductionLines.AddRange(lines);
|
||
dbContext.Partners.AddRange(
|
||
new Partner { Id = Guid.NewGuid(), Code = "SUP-001", Name = "东方重工协作厂", PartnerType = "outsourcer" },
|
||
new Partner { Id = Guid.NewGuid(), Code = "SUP-002", Name = "华东液压", PartnerType = "supplier" },
|
||
new Partner { Id = Guid.NewGuid(), Code = "SUP-003", Name = "水工智控", PartnerType = "supplier" });
|
||
|
||
var now = DateTimeOffset.UtcNow;
|
||
for (var orderIndex = 0; orderIndex < Orders.Length; orderIndex++)
|
||
{
|
||
var blueprint = Orders[orderIndex];
|
||
var order = new ProductionOrder
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Code = blueprint.Code,
|
||
ProductName = blueprint.ProductName,
|
||
DataSource = "demo",
|
||
BatchQty = blueprint.BatchQty,
|
||
RequiredQty = blueprint.BatchQty,
|
||
PlannedStart = new DateTime(2026, 7, 1).AddDays(orderIndex % 4),
|
||
PlannedEnd = new DateTime(2026, 7, 14).AddDays(orderIndex),
|
||
LineId = lines[orderIndex].Id,
|
||
LineName = blueprint.LineName,
|
||
Owner = blueprint.Owner,
|
||
DailyDelta = blueprint.DailyDelta,
|
||
ThumbnailKey = blueprint.ThumbnailKey,
|
||
CreatedAt = now.AddDays(-10 - orderIndex),
|
||
UpdatedAt = now,
|
||
RowVersion = Guid.NewGuid().ToByteArray(),
|
||
};
|
||
var nodes = BuildNodes(order, blueprint, orderIndex, now);
|
||
aggregationService.Recalculate(order, nodes);
|
||
order.PlanAchievement = Math.Clamp(order.Progress + 8 - order.DelayDays * 1.5, 0, 120);
|
||
|
||
dbContext.ProductionOrders.Add(order);
|
||
dbContext.ProductionNodes.AddRange(nodes);
|
||
dbContext.ProductionTrendPoints.AddRange(BuildTrend(order, orderIndex, now));
|
||
dbContext.RiskEvents.AddRange(BuildRiskEvents(order, nodes, now));
|
||
}
|
||
|
||
await dbContext.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
private static List<ProductionNode> BuildNodes(
|
||
ProductionOrder order,
|
||
OrderBlueprint blueprint,
|
||
int orderIndex,
|
||
DateTimeOffset now)
|
||
{
|
||
var nodes = new List<ProductionNode>();
|
||
var root = CreateNode(
|
||
order,
|
||
null,
|
||
"ROOT",
|
||
order.ProductName,
|
||
0,
|
||
"order",
|
||
"self_made",
|
||
order.BatchQty,
|
||
0,
|
||
order.Owner,
|
||
"东方水利",
|
||
blueprint.ThumbnailKey,
|
||
0,
|
||
order.PlannedStart!.Value,
|
||
order.PlannedEnd!.Value,
|
||
now);
|
||
nodes.Add(root);
|
||
|
||
for (var partIndex = 0; partIndex < Parts.Length; partIndex++)
|
||
{
|
||
var part = Parts[partIndex];
|
||
var partNode = CreateNode(
|
||
order,
|
||
root.Id,
|
||
$"P-{partIndex + 1:00}",
|
||
part.Name,
|
||
1,
|
||
"part",
|
||
part.SupplyType,
|
||
order.BatchQty,
|
||
partIndex,
|
||
part.Owner,
|
||
part.Vendor,
|
||
part.VisualKey,
|
||
partIndex,
|
||
order.PlannedStart!.Value.AddDays(partIndex),
|
||
order.PlannedEnd!.Value.AddDays(partIndex - Parts.Length),
|
||
now);
|
||
nodes.Add(partNode);
|
||
|
||
for (var componentIndex = 0; componentIndex < part.Components.Length; componentIndex++)
|
||
{
|
||
var component = part.Components[componentIndex];
|
||
var componentNode = CreateNode(
|
||
order,
|
||
partNode.Id,
|
||
$"C-{partIndex + 1:00}-{componentIndex + 1:00}",
|
||
component.Name,
|
||
2,
|
||
"component",
|
||
component.SupplyType,
|
||
order.BatchQty,
|
||
componentIndex,
|
||
component.Owner,
|
||
component.Vendor,
|
||
component.VisualKey,
|
||
componentIndex,
|
||
partNode.PlannedStart!.Value.AddDays(componentIndex),
|
||
partNode.PlannedEnd!.Value.AddDays(componentIndex - 2),
|
||
now);
|
||
nodes.Add(componentNode);
|
||
|
||
for (var processIndex = 0; processIndex < ProcessNames.Length; processIndex++)
|
||
{
|
||
var signature = orderIndex * 17 + partIndex * 11 + componentIndex * 7 + processIndex * 5;
|
||
var progress = Math.Clamp(blueprint.BaseProgress + signature % 29 - 14 - processIndex * 4, 3, 100);
|
||
var critical = signature % 13 == 0;
|
||
var warning = !critical && (signature % 5 == 0 || progress < 45);
|
||
var delayDays = critical ? 3 + signature % 4 : warning ? 1 : 0;
|
||
var status = progress >= 100
|
||
? "done"
|
||
: critical && processIndex == 2
|
||
? "blocked"
|
||
: delayDays > 0
|
||
? "delayed"
|
||
: "in_progress";
|
||
var risk = critical ? "critical" : warning ? "warning" : "normal";
|
||
var requiredQty = order.BatchQty;
|
||
var completedQty = (int)Math.Round(requiredQty * progress / 100d, MidpointRounding.AwayFromZero);
|
||
var processNode = CreateNode(
|
||
order,
|
||
componentNode.Id,
|
||
$"OP-{(processIndex + 1) * 10:00}",
|
||
$"{component.Name} - {ProcessNames[processIndex]}",
|
||
3,
|
||
"process",
|
||
component.SupplyType,
|
||
requiredQty,
|
||
processIndex,
|
||
component.Owner,
|
||
component.Vendor,
|
||
processIndex == 2 ? "package" : component.VisualKey,
|
||
processIndex,
|
||
componentNode.PlannedStart!.Value.AddDays(processIndex),
|
||
componentNode.PlannedEnd!.Value.AddDays(processIndex - 2),
|
||
now);
|
||
processNode.CompletedQty = completedQty;
|
||
processNode.DefectQty = Math.Min(completedQty, risk == "critical" ? Math.Max(1, completedQty / 50) : completedQty / 250);
|
||
processNode.Progress = progress;
|
||
processNode.Status = status;
|
||
processNode.RiskLevel = risk;
|
||
processNode.DelayDays = delayDays;
|
||
processNode.DelayReason = DelayReason(risk, component.SupplyType, ProcessNames[processIndex]);
|
||
processNode.ActualStart = progress > 0 ? processNode.PlannedStart : null;
|
||
processNode.ActualEnd = progress >= 100 ? processNode.PlannedEnd : null;
|
||
nodes.Add(processNode);
|
||
}
|
||
}
|
||
}
|
||
|
||
return nodes;
|
||
}
|
||
|
||
private static ProductionNode CreateNode(
|
||
ProductionOrder order,
|
||
Guid? parentId,
|
||
string code,
|
||
string name,
|
||
int level,
|
||
string nodeType,
|
||
string supplyType,
|
||
int requiredQty,
|
||
int codeIndex,
|
||
string owner,
|
||
string vendor,
|
||
string visualKey,
|
||
int sortOrder,
|
||
DateTime plannedStart,
|
||
DateTime plannedEnd,
|
||
DateTimeOffset now)
|
||
{
|
||
return new ProductionNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
OrderId = order.Id,
|
||
ParentNodeId = parentId,
|
||
Code = code,
|
||
OperationCode = code,
|
||
MaterialCode = $"MAT-{order.Code[^3..]}-{level}{codeIndex + 1:00}",
|
||
Name = name,
|
||
Level = level,
|
||
NodeType = nodeType,
|
||
SupplyType = supplyType,
|
||
RequiredQty = requiredQty,
|
||
Owner = owner,
|
||
Vendor = vendor,
|
||
StationName = $"{owner}-{sortOrder + 1}",
|
||
VisualKey = visualKey,
|
||
SortOrder = sortOrder,
|
||
PlannedStart = plannedStart,
|
||
PlannedEnd = plannedEnd,
|
||
UpdatedAt = now,
|
||
RowVersion = Guid.NewGuid().ToByteArray(),
|
||
};
|
||
}
|
||
|
||
private static IEnumerable<ProductionTrendPoint> BuildTrend(
|
||
ProductionOrder order,
|
||
int orderIndex,
|
||
DateTimeOffset now)
|
||
{
|
||
for (var index = 0; index < 12; index++)
|
||
{
|
||
var completion = Math.Clamp(order.Progress - (11 - index) * 2 + (index + orderIndex) % 3, 0, 100);
|
||
var actual = (int)Math.Round(order.RequiredQty * completion / 100d);
|
||
var planned = Math.Max(actual, (int)Math.Round(order.RequiredQty * Math.Min(100, completion + 8) / 100d));
|
||
yield return new ProductionTrendPoint
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
OrderId = order.Id,
|
||
SampleTime = now.AddMinutes((index - 11) * 8),
|
||
Completion = completion,
|
||
Risk = Math.Max(1, order.DelayDays + 11 - index),
|
||
PlannedQty = planned,
|
||
ActualQty = actual,
|
||
Achievement = planned == 0 ? 0 : Math.Min(120, actual * 100d / planned),
|
||
};
|
||
}
|
||
}
|
||
|
||
private static IEnumerable<RiskEvent> BuildRiskEvents(
|
||
ProductionOrder order,
|
||
IReadOnlyCollection<ProductionNode> nodes,
|
||
DateTimeOffset now)
|
||
{
|
||
return nodes
|
||
.Where(node => node.RiskLevel != "normal" || node.DelayDays > 0)
|
||
.OrderByDescending(node => node.RiskLevel == "critical")
|
||
.ThenByDescending(node => node.DelayDays)
|
||
.Take(8)
|
||
.Select((node, index) => new RiskEvent
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
OrderId = order.Id,
|
||
NodeId = node.Id,
|
||
OrderCode = order.Code,
|
||
NodeName = node.Name,
|
||
RiskLevel = node.RiskLevel,
|
||
Message = string.IsNullOrWhiteSpace(node.DelayReason)
|
||
? $"计划偏差 {Math.Max(1, node.DelayDays)} 天,需要复核节拍"
|
||
: node.DelayReason,
|
||
HandlingStatus = "processing",
|
||
OccurredAt = now.AddMinutes(-index),
|
||
});
|
||
}
|
||
|
||
private static string DelayReason(string riskLevel, string supplyType, string processName)
|
||
{
|
||
if (riskLevel == "normal")
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return supplyType switch
|
||
{
|
||
"outsourced" => $"{processName}外协回货节拍低于计划",
|
||
"purchased" => $"{processName}供应到料存在批次差异",
|
||
_ => $"{processName}工序良率波动,需要复检",
|
||
};
|
||
}
|
||
|
||
private sealed record OrderBlueprint(
|
||
string Code,
|
||
string ProductName,
|
||
int BatchQty,
|
||
string LineName,
|
||
string Owner,
|
||
int BaseProgress,
|
||
double DailyDelta,
|
||
string ThumbnailKey);
|
||
|
||
private sealed record PartBlueprint(
|
||
string Name,
|
||
string SupplyType,
|
||
string Owner,
|
||
string Vendor,
|
||
string VisualKey,
|
||
ComponentBlueprint[] Components);
|
||
|
||
private sealed record ComponentBlueprint(
|
||
string Name,
|
||
string SupplyType,
|
||
string Owner,
|
||
string Vendor,
|
||
string VisualKey);
|
||
}
|