feat: initialize manufacturing progress dashboard
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Contracts;
|
||||
|
||||
public sealed record ProductionNodeDto(
|
||||
Guid Id,
|
||||
Guid OrderId,
|
||||
Guid? ParentId,
|
||||
string Code,
|
||||
string OperationCode,
|
||||
string MaterialCode,
|
||||
string Name,
|
||||
string SourceSequence,
|
||||
string Specification,
|
||||
string Material,
|
||||
decimal? UnitWeight,
|
||||
decimal? TotalWeight,
|
||||
string UnitWeightText,
|
||||
string TotalWeightText,
|
||||
decimal? BomQuantity,
|
||||
string BomQuantityText,
|
||||
string Remark,
|
||||
string SourceSheet,
|
||||
int SourceRow,
|
||||
int Level,
|
||||
string NodeType,
|
||||
string SupplyType,
|
||||
int RequiredQty,
|
||||
int CompletedQty,
|
||||
int DefectQty,
|
||||
int Progress,
|
||||
string Status,
|
||||
string RiskLevel,
|
||||
int DelayDays,
|
||||
string DelayReason,
|
||||
string PlannedStart,
|
||||
string PlannedEnd,
|
||||
string ActualStart,
|
||||
string ActualEnd,
|
||||
string Owner,
|
||||
string Vendor,
|
||||
string StationName,
|
||||
string VisualKey,
|
||||
string Version,
|
||||
IReadOnlyList<ProductionNodeDto> Children);
|
||||
|
||||
public sealed record TrendPointDto(
|
||||
string Time,
|
||||
int Completion,
|
||||
int Risk,
|
||||
int PlannedQty,
|
||||
int ActualQty,
|
||||
double Achievement);
|
||||
|
||||
public sealed record RiskEventDto(
|
||||
Guid Id,
|
||||
Guid OrderId,
|
||||
Guid NodeId,
|
||||
string OrderCode,
|
||||
string NodeName,
|
||||
string RiskLevel,
|
||||
string Message,
|
||||
string Time,
|
||||
string HandlingStatus);
|
||||
|
||||
public sealed record OrderSummaryDto(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string ProductName,
|
||||
int BatchQty,
|
||||
int RequiredQty,
|
||||
int CompletedQty,
|
||||
int Progress,
|
||||
string Status,
|
||||
string RiskLevel,
|
||||
int DelayDays,
|
||||
string PlannedStart,
|
||||
string PlannedEnd,
|
||||
string LineName,
|
||||
string Owner,
|
||||
double PlanAchievement,
|
||||
double DailyDelta,
|
||||
string ThumbnailKey,
|
||||
ProductionNodeDto Root,
|
||||
IReadOnlyList<TrendPointDto> Trend,
|
||||
IReadOnlyList<RiskEventDto> Events);
|
||||
|
||||
public sealed record MaterialQuotaDto(
|
||||
Guid Id,
|
||||
Guid OrderId,
|
||||
string Category,
|
||||
string SourceSequence,
|
||||
string MaterialCode,
|
||||
string MaterialName,
|
||||
string Specification,
|
||||
string Unit,
|
||||
decimal? Quantity,
|
||||
decimal? NetWeight,
|
||||
decimal? ConsumptionQuota,
|
||||
string QuantityText,
|
||||
string NetWeightText,
|
||||
string ConsumptionQuotaText,
|
||||
string Brand,
|
||||
string Remark,
|
||||
string SourceSheet,
|
||||
int SourceRow);
|
||||
|
||||
public sealed record PagedResult<T>(
|
||||
IReadOnlyList<T> Items,
|
||||
int Total,
|
||||
int Page,
|
||||
int PageSize);
|
||||
|
||||
public sealed record DashboardOverviewDto(
|
||||
int TotalOrders,
|
||||
int TotalRequiredQty,
|
||||
int TotalCompletedQty,
|
||||
double OverallProgress,
|
||||
int DelayedOrders,
|
||||
int CriticalOrders,
|
||||
int WarningOrders,
|
||||
double PlanAchievement,
|
||||
DateTimeOffset RefreshedAt);
|
||||
|
||||
public sealed record DelayTopItemDto(
|
||||
int Rank,
|
||||
Guid OrderId,
|
||||
string OrderCode,
|
||||
string ProductName,
|
||||
int DelayDays,
|
||||
string DelayReason,
|
||||
string RiskLevel);
|
||||
|
||||
public sealed record UpdateNodeProgressRequest(
|
||||
int? CompletedQty,
|
||||
string? ExpectedVersion,
|
||||
string? Status,
|
||||
string? RiskLevel,
|
||||
int? DelayDays,
|
||||
string? DelayReason,
|
||||
int? DefectQty,
|
||||
DateTime? ActualStart,
|
||||
DateTime? ActualEnd);
|
||||
|
||||
public sealed record UpdateNodeProgressResultDto(
|
||||
ProductionNodeDto Node,
|
||||
OrderSummaryDto Order,
|
||||
RiskEventDto? RiskEvent,
|
||||
TrendPointDto TrendPoint);
|
||||
@@ -0,0 +1,172 @@
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data;
|
||||
|
||||
public sealed class DashboardDbContext(DbContextOptions<DashboardDbContext> options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public DbSet<ProductionOrder> ProductionOrders => Set<ProductionOrder>();
|
||||
public DbSet<ProductionNode> ProductionNodes => Set<ProductionNode>();
|
||||
public DbSet<ProductionTrendPoint> ProductionTrendPoints => Set<ProductionTrendPoint>();
|
||||
public DbSet<RiskEvent> RiskEvents => Set<RiskEvent>();
|
||||
public DbSet<ProductionLine> ProductionLines => Set<ProductionLine>();
|
||||
public DbSet<Partner> Partners => Set<Partner>();
|
||||
public DbSet<MaterialQuota> MaterialQuotas => Set<MaterialQuota>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ProductionOrder>(entity =>
|
||||
{
|
||||
entity.ToTable("ProductionOrders");
|
||||
entity.HasKey(order => order.Id);
|
||||
entity.HasIndex(order => order.Code);
|
||||
entity.HasIndex(order => new { order.Status, order.RiskLevel, order.PlannedEnd });
|
||||
entity.Property(order => order.Code).HasMaxLength(40);
|
||||
entity.Property(order => order.ProductName).HasMaxLength(200);
|
||||
entity.Property(order => order.DataSource).HasMaxLength(32);
|
||||
entity.Property(order => order.SourceDocumentCode).HasMaxLength(80);
|
||||
entity.Property(order => order.ProjectName).HasMaxLength(200);
|
||||
entity.Property(order => order.DrawingProductName).HasMaxLength(240);
|
||||
entity.Property(order => order.SourceFileName).HasMaxLength(260);
|
||||
entity.Property(order => order.SourceFileHash).HasMaxLength(64);
|
||||
entity.HasIndex(order => order.SourceDocumentCode)
|
||||
.IsUnique()
|
||||
.HasFilter("[SourceDocumentCode] <> ''");
|
||||
entity.Property(order => order.Status).HasMaxLength(32);
|
||||
entity.Property(order => order.RiskLevel).HasMaxLength(32);
|
||||
entity.Property(order => order.LineName).HasMaxLength(80);
|
||||
entity.Property(order => order.Owner).HasMaxLength(80);
|
||||
entity.Property(order => order.ThumbnailKey).HasMaxLength(40);
|
||||
entity.Property(order => order.RowVersion).IsRowVersion();
|
||||
entity.HasOne<ProductionLine>()
|
||||
.WithMany()
|
||||
.HasForeignKey(order => order.LineId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ProductionNode>(entity =>
|
||||
{
|
||||
entity.ToTable("ProductionNodes");
|
||||
entity.HasKey(node => node.Id);
|
||||
entity.HasIndex(node => new { node.OrderId, node.ParentNodeId, node.SortOrder });
|
||||
entity.HasIndex(node => new { node.OrderId, node.NodeType, node.RiskLevel, node.Status });
|
||||
entity.Property(node => node.Code).HasMaxLength(60);
|
||||
entity.Property(node => node.OperationCode).HasMaxLength(60);
|
||||
entity.Property(node => node.MaterialCode).HasMaxLength(80);
|
||||
entity.Property(node => node.Name).HasMaxLength(240);
|
||||
entity.Property(node => node.SourceSequence).HasMaxLength(40);
|
||||
entity.Property(node => node.Specification).HasMaxLength(240);
|
||||
entity.Property(node => node.Material).HasMaxLength(120);
|
||||
entity.Property(node => node.UnitWeight).HasPrecision(18, 4);
|
||||
entity.Property(node => node.TotalWeight).HasPrecision(18, 4);
|
||||
entity.Property(node => node.UnitWeightText).HasMaxLength(80);
|
||||
entity.Property(node => node.TotalWeightText).HasMaxLength(80);
|
||||
entity.Property(node => node.BomQuantity).HasPrecision(18, 4);
|
||||
entity.Property(node => node.BomQuantityText).HasMaxLength(80);
|
||||
entity.Property(node => node.Remark).HasMaxLength(500);
|
||||
entity.Property(node => node.SourceSheet).HasMaxLength(40);
|
||||
entity.Property(node => node.NodeType).HasMaxLength(32);
|
||||
entity.Property(node => node.SupplyType).HasMaxLength(32);
|
||||
entity.Property(node => node.Status).HasMaxLength(32);
|
||||
entity.Property(node => node.RiskLevel).HasMaxLength(32);
|
||||
entity.Property(node => node.DelayReason).HasMaxLength(500);
|
||||
entity.Property(node => node.Owner).HasMaxLength(100);
|
||||
entity.Property(node => node.Vendor).HasMaxLength(120);
|
||||
entity.Property(node => node.StationName).HasMaxLength(120);
|
||||
entity.Property(node => node.VisualKey).HasMaxLength(40);
|
||||
entity.Property(node => node.RowVersion).IsRowVersion();
|
||||
entity.HasAlternateKey(node => new { node.OrderId, node.Id });
|
||||
entity.HasOne<ProductionOrder>()
|
||||
.WithMany()
|
||||
.HasForeignKey(node => node.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne<ProductionNode>()
|
||||
.WithMany()
|
||||
.HasForeignKey(node => new { node.OrderId, node.ParentNodeId })
|
||||
.HasPrincipalKey(node => new { node.OrderId, node.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ProductionTrendPoint>(entity =>
|
||||
{
|
||||
entity.ToTable("ProductionTrendPoints");
|
||||
entity.HasKey(point => point.Id);
|
||||
entity.HasIndex(point => new { point.OrderId, point.SampleTime }).IsDescending(false, true);
|
||||
entity.HasOne<ProductionOrder>()
|
||||
.WithMany()
|
||||
.HasForeignKey(point => point.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RiskEvent>(entity =>
|
||||
{
|
||||
entity.ToTable("RiskEvents");
|
||||
entity.HasKey(riskEvent => riskEvent.Id);
|
||||
entity.HasIndex(riskEvent => new { riskEvent.OrderId, riskEvent.OccurredAt }).IsDescending(false, true);
|
||||
entity.Property(riskEvent => riskEvent.OrderCode).HasMaxLength(40);
|
||||
entity.Property(riskEvent => riskEvent.NodeName).HasMaxLength(240);
|
||||
entity.Property(riskEvent => riskEvent.RiskLevel).HasMaxLength(32);
|
||||
entity.Property(riskEvent => riskEvent.Message).HasMaxLength(500);
|
||||
entity.Property(riskEvent => riskEvent.HandlingStatus).HasMaxLength(32);
|
||||
entity.HasOne<ProductionOrder>()
|
||||
.WithMany()
|
||||
.HasForeignKey(riskEvent => riskEvent.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne<ProductionNode>()
|
||||
.WithMany()
|
||||
.HasForeignKey(riskEvent => new { riskEvent.OrderId, riskEvent.NodeId })
|
||||
.HasPrincipalKey(node => new { node.OrderId, node.Id })
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<MaterialQuota>(entity =>
|
||||
{
|
||||
entity.ToTable("MaterialQuotas");
|
||||
entity.HasKey(item => item.Id);
|
||||
entity.HasIndex(item => new { item.OrderId, item.SourceSheet, item.SourceRow }).IsUnique();
|
||||
entity.HasIndex(item => new { item.OrderId, item.Category, item.MaterialCode });
|
||||
entity.Property(item => item.Category).HasMaxLength(80);
|
||||
entity.Property(item => item.SourceSequence).HasMaxLength(40);
|
||||
entity.Property(item => item.MaterialCode).HasMaxLength(80);
|
||||
entity.Property(item => item.MaterialName).HasMaxLength(240);
|
||||
entity.Property(item => item.Specification).HasMaxLength(240);
|
||||
entity.Property(item => item.Unit).HasMaxLength(24);
|
||||
entity.Property(item => item.Quantity).HasPrecision(18, 4);
|
||||
entity.Property(item => item.NetWeight).HasPrecision(18, 4);
|
||||
entity.Property(item => item.ConsumptionQuota).HasPrecision(18, 4);
|
||||
entity.Property(item => item.QuantityText).HasMaxLength(80);
|
||||
entity.Property(item => item.NetWeightText).HasMaxLength(80);
|
||||
entity.Property(item => item.ConsumptionQuotaText).HasMaxLength(80);
|
||||
entity.Property(item => item.Brand).HasMaxLength(120);
|
||||
entity.Property(item => item.Remark).HasMaxLength(500);
|
||||
entity.Property(item => item.SourceSheet).HasMaxLength(40);
|
||||
entity.HasOne<ProductionOrder>()
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ProductionLine>(entity =>
|
||||
{
|
||||
entity.ToTable("ProductionLines");
|
||||
entity.HasKey(line => line.Id);
|
||||
entity.HasIndex(line => line.Code).IsUnique();
|
||||
entity.Property(line => line.Code).HasMaxLength(40);
|
||||
entity.Property(line => line.Name).HasMaxLength(80);
|
||||
entity.Property(line => line.Owner).HasMaxLength(80);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Partner>(entity =>
|
||||
{
|
||||
entity.ToTable("Partners");
|
||||
entity.HasKey(partner => partner.Id);
|
||||
entity.HasIndex(partner => partner.Code).IsUnique();
|
||||
entity.Property(partner => partner.Code).HasMaxLength(40);
|
||||
entity.Property(partner => partner.Name).HasMaxLength(120);
|
||||
entity.Property(partner => partner.PartnerType).HasMaxLength(32);
|
||||
entity.Property(partner => partner.ContactName).HasMaxLength(80);
|
||||
entity.Property(partner => partner.ContactPhone).HasMaxLength(32);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data;
|
||||
|
||||
public static class DatabaseInitializer
|
||||
{
|
||||
public static async Task InitializeAsync(WebApplication app)
|
||||
{
|
||||
var autoMigrate = app.Configuration.GetValue("Database:AutoMigrate", false);
|
||||
var seed = app.Configuration.GetValue("Database:Seed", false);
|
||||
if (!autoMigrate && !seed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var scope = app.Services.CreateAsyncScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
|
||||
|
||||
if (autoMigrate)
|
||||
{
|
||||
if (dbContext.Database.IsRelational())
|
||||
{
|
||||
await dbContext.Database.MigrateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (seed)
|
||||
{
|
||||
await scope.ServiceProvider.GetRequiredService<DashboardSeeder>().SeedAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+457
@@ -0,0 +1,457 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DashboardDbContext))]
|
||||
[Migration("20260710025438_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.20")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.Partner", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("ContactPhone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("PartnerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Partners", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProductionLines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DefectQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DelayReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("NodeType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("OperationCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid?>("ParentNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SupplyType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Vendor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("VisualKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "ParentNodeId", "SortOrder");
|
||||
|
||||
b.HasIndex("OrderId", "NodeType", "RiskLevel", "Status");
|
||||
|
||||
b.ToTable("ProductionNodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("BatchQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<double>("DailyDelta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("LineId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<double>("PlanAchievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("ThumbnailKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("LineId");
|
||||
|
||||
b.HasIndex("Status", "RiskLevel", "PlannedEnd");
|
||||
|
||||
b.ToTable("ProductionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Achievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("ActualQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Completion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("PlannedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Risk")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("SampleTime")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "SampleTime")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("ProductionTrendPoints", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("HandlingStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset>("OccurredAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("OrderCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "NodeId");
|
||||
|
||||
b.HasIndex("OrderId", "OccurredAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("RiskEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "ParentNodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionLine", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LineId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "NodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Partners",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
PartnerType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
ContactName = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
ContactPhone = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Partners", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductionLines",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
Owner = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProductionLines", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductionOrders",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
ProductName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
BatchQty = table.Column<int>(type: "int", nullable: false),
|
||||
RequiredQty = table.Column<int>(type: "int", nullable: false),
|
||||
CompletedQty = table.Column<int>(type: "int", nullable: false),
|
||||
Progress = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
RiskLevel = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
DelayDays = table.Column<int>(type: "int", nullable: false),
|
||||
PlannedStart = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
PlannedEnd = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
LineId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LineName = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
Owner = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
PlanAchievement = table.Column<double>(type: "float", nullable: false),
|
||||
DailyDelta = table.Column<double>(type: "float", nullable: false),
|
||||
ThumbnailKey = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
RowVersion = table.Column<byte[]>(type: "rowversion", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProductionOrders", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProductionOrders_ProductionLines_LineId",
|
||||
column: x => x.LineId,
|
||||
principalTable: "ProductionLines",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductionNodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ParentNodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Code = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: false),
|
||||
OperationCode = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: false),
|
||||
MaterialCode = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(240)", maxLength: 240, nullable: false),
|
||||
Level = table.Column<int>(type: "int", nullable: false),
|
||||
NodeType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
SupplyType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
RequiredQty = table.Column<int>(type: "int", nullable: false),
|
||||
CompletedQty = table.Column<int>(type: "int", nullable: false),
|
||||
DefectQty = table.Column<int>(type: "int", nullable: false),
|
||||
Progress = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
RiskLevel = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
DelayDays = table.Column<int>(type: "int", nullable: false),
|
||||
DelayReason = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
PlannedStart = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
PlannedEnd = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ActualStart = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ActualEnd = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Owner = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Vendor = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
StationName = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
VisualKey = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
RowVersion = table.Column<byte[]>(type: "rowversion", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProductionNodes", x => x.Id);
|
||||
table.UniqueConstraint("AK_ProductionNodes_OrderId_Id", x => new { x.OrderId, x.Id });
|
||||
table.ForeignKey(
|
||||
name: "FK_ProductionNodes_ProductionNodes_OrderId_ParentNodeId",
|
||||
columns: x => new { x.OrderId, x.ParentNodeId },
|
||||
principalTable: "ProductionNodes",
|
||||
principalColumns: new[] { "OrderId", "Id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProductionNodes_ProductionOrders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "ProductionOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductionTrendPoints",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SampleTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
Completion = table.Column<int>(type: "int", nullable: false),
|
||||
Risk = table.Column<int>(type: "int", nullable: false),
|
||||
PlannedQty = table.Column<int>(type: "int", nullable: false),
|
||||
ActualQty = table.Column<int>(type: "int", nullable: false),
|
||||
Achievement = table.Column<double>(type: "float", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProductionTrendPoints", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProductionTrendPoints_ProductionOrders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "ProductionOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RiskEvents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OrderCode = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
NodeName = table.Column<string>(type: "nvarchar(240)", maxLength: 240, nullable: false),
|
||||
RiskLevel = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Message = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
HandlingStatus = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
OccurredAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RiskEvents", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RiskEvents_ProductionNodes_OrderId_NodeId",
|
||||
columns: x => new { x.OrderId, x.NodeId },
|
||||
principalTable: "ProductionNodes",
|
||||
principalColumns: new[] { "OrderId", "Id" });
|
||||
table.ForeignKey(
|
||||
name: "FK_RiskEvents_ProductionOrders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "ProductionOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Partners_Code",
|
||||
table: "Partners",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionLines_Code",
|
||||
table: "ProductionLines",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionNodes_OrderId_NodeType_RiskLevel_Status",
|
||||
table: "ProductionNodes",
|
||||
columns: new[] { "OrderId", "NodeType", "RiskLevel", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionNodes_OrderId_ParentNodeId_SortOrder",
|
||||
table: "ProductionNodes",
|
||||
columns: new[] { "OrderId", "ParentNodeId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_Code",
|
||||
table: "ProductionOrders",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_LineId",
|
||||
table: "ProductionOrders",
|
||||
column: "LineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_Status_RiskLevel_PlannedEnd",
|
||||
table: "ProductionOrders",
|
||||
columns: new[] { "Status", "RiskLevel", "PlannedEnd" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionTrendPoints_OrderId_SampleTime",
|
||||
table: "ProductionTrendPoints",
|
||||
columns: new[] { "OrderId", "SampleTime" },
|
||||
descending: new[] { false, true });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RiskEvents_OrderId_NodeId",
|
||||
table: "RiskEvents",
|
||||
columns: new[] { "OrderId", "NodeId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RiskEvents_OrderId_OccurredAt",
|
||||
table: "RiskEvents",
|
||||
columns: new[] { "OrderId", "OccurredAt" },
|
||||
descending: new[] { false, true });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Partners");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProductionTrendPoints");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RiskEvents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProductionLines");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+652
@@ -0,0 +1,652 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DashboardDbContext))]
|
||||
[Migration("20260710095939_ImportRealBom")]
|
||||
partial class ImportRealBom
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.20")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("ConsumptionQuota")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("ConsumptionQuotaText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<decimal?>("NetWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("NetWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<decimal?>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("QuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("Unit")
|
||||
.IsRequired()
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("nvarchar(24)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "Category", "MaterialCode");
|
||||
|
||||
b.HasIndex("OrderId", "SourceSheet", "SourceRow")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MaterialQuotas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.Partner", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("ContactPhone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("PartnerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Partners", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProductionLines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("BomQuantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("BomQuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DefectQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DelayReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("NodeType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("OperationCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid?>("ParentNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SupplyType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<decimal?>("TotalWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("TotalWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("UnitWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("UnitWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Vendor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("VisualKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "ParentNodeId", "SortOrder");
|
||||
|
||||
b.HasIndex("OrderId", "NodeType", "RiskLevel", "Status");
|
||||
|
||||
b.ToTable("ProductionNodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("BatchQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<double>("DailyDelta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DataSource")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ImportedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<Guid?>("LineId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<double>("PlanAchievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SourceDocumentCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("SourceFileHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(260)
|
||||
.HasColumnType("nvarchar(260)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("ThumbnailKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("LineId");
|
||||
|
||||
b.HasIndex("SourceDocumentCode")
|
||||
.IsUnique()
|
||||
.HasFilter("[SourceDocumentCode] <> ''");
|
||||
|
||||
b.HasIndex("Status", "RiskLevel", "PlannedEnd");
|
||||
|
||||
b.ToTable("ProductionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Achievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("ActualQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Completion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("PlannedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Risk")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("SampleTime")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "SampleTime")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("ProductionTrendPoints", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("HandlingStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset>("OccurredAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("OrderCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "NodeId");
|
||||
|
||||
b.HasIndex("OrderId", "OccurredAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("RiskEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "ParentNodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionLine", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LineId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "NodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ImportRealBom : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedStart",
|
||||
table: "ProductionOrders",
|
||||
type: "datetime2",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedEnd",
|
||||
table: "ProductionOrders",
|
||||
type: "datetime2",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DataSource",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(32)",
|
||||
maxLength: 32,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DrawingProductName",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(240)",
|
||||
maxLength: 240,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "ImportedAt",
|
||||
table: "ProductionOrders",
|
||||
type: "datetimeoffset",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProjectName",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(200)",
|
||||
maxLength: 200,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SourceDocumentCode",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(80)",
|
||||
maxLength: 80,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SourceFileHash",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SourceFileName",
|
||||
table: "ProductionOrders",
|
||||
type: "nvarchar(260)",
|
||||
maxLength: 260,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [ProductionOrders] SET [DataSource] = 'demo' WHERE [Code] LIKE 'MO-260708-%';");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedStart",
|
||||
table: "ProductionNodes",
|
||||
type: "datetime2",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedEnd",
|
||||
table: "ProductionNodes",
|
||||
type: "datetime2",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "BomQuantity",
|
||||
table: "ProductionNodes",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BomQuantityText",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(80)",
|
||||
maxLength: 80,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Material",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(120)",
|
||||
maxLength: 120,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Remark",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SourceRow",
|
||||
table: "ProductionNodes",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SourceSequence",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(40)",
|
||||
maxLength: 40,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SourceSheet",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(40)",
|
||||
maxLength: 40,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Specification",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(240)",
|
||||
maxLength: 240,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "TotalWeight",
|
||||
table: "ProductionNodes",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TotalWeightText",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(80)",
|
||||
maxLength: 80,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "UnitWeight",
|
||||
table: "ProductionNodes",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UnitWeightText",
|
||||
table: "ProductionNodes",
|
||||
type: "nvarchar(80)",
|
||||
maxLength: 80,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MaterialQuotas",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Category = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
SourceSequence = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
MaterialCode = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
MaterialName = table.Column<string>(type: "nvarchar(240)", maxLength: 240, nullable: false),
|
||||
Specification = table.Column<string>(type: "nvarchar(240)", maxLength: 240, nullable: false),
|
||||
Unit = table.Column<string>(type: "nvarchar(24)", maxLength: 24, nullable: false),
|
||||
Quantity = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: true),
|
||||
NetWeight = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: true),
|
||||
ConsumptionQuota = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: true),
|
||||
QuantityText = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
NetWeightText = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
ConsumptionQuotaText = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
Brand = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
Remark = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
SourceSheet = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
SourceRow = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MaterialQuotas", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MaterialQuotas_ProductionOrders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "ProductionOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_SourceDocumentCode",
|
||||
table: "ProductionOrders",
|
||||
column: "SourceDocumentCode",
|
||||
unique: true,
|
||||
filter: "[SourceDocumentCode] <> ''");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MaterialQuotas_OrderId_Category_MaterialCode",
|
||||
table: "MaterialQuotas",
|
||||
columns: new[] { "OrderId", "Category", "MaterialCode" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MaterialQuotas_OrderId_SourceSheet_SourceRow",
|
||||
table: "MaterialQuotas",
|
||||
columns: new[] { "OrderId", "SourceSheet", "SourceRow" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "MaterialQuotas");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProductionOrders_SourceDocumentCode",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DataSource",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrawingProductName",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ImportedAt",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectName",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceDocumentCode",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceFileHash",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceFileName",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BomQuantity",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BomQuantityText",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Material",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Remark",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceRow",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceSequence",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceSheet",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Specification",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TotalWeight",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TotalWeightText",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UnitWeight",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UnitWeightText",
|
||||
table: "ProductionNodes");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedStart",
|
||||
table: "ProductionOrders",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedEnd",
|
||||
table: "ProductionOrders",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedStart",
|
||||
table: "ProductionNodes",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "PlannedEnd",
|
||||
table: "ProductionNodes",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "datetime2",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+651
@@ -0,0 +1,651 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DashboardDbContext))]
|
||||
[Migration("20260710103033_AllowRepeatedWorkOrderCodes")]
|
||||
partial class AllowRepeatedWorkOrderCodes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.20")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("ConsumptionQuota")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("ConsumptionQuotaText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<decimal?>("NetWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("NetWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<decimal?>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("QuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("Unit")
|
||||
.IsRequired()
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("nvarchar(24)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "Category", "MaterialCode");
|
||||
|
||||
b.HasIndex("OrderId", "SourceSheet", "SourceRow")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MaterialQuotas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.Partner", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("ContactPhone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("PartnerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Partners", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProductionLines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("BomQuantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("BomQuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DefectQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DelayReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("NodeType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("OperationCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid?>("ParentNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SupplyType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<decimal?>("TotalWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("TotalWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("UnitWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("UnitWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Vendor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("VisualKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "ParentNodeId", "SortOrder");
|
||||
|
||||
b.HasIndex("OrderId", "NodeType", "RiskLevel", "Status");
|
||||
|
||||
b.ToTable("ProductionNodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("BatchQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<double>("DailyDelta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DataSource")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ImportedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<Guid?>("LineId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<double>("PlanAchievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SourceDocumentCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("SourceFileHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(260)
|
||||
.HasColumnType("nvarchar(260)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("ThumbnailKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.HasIndex("LineId");
|
||||
|
||||
b.HasIndex("SourceDocumentCode")
|
||||
.IsUnique()
|
||||
.HasFilter("[SourceDocumentCode] <> ''");
|
||||
|
||||
b.HasIndex("Status", "RiskLevel", "PlannedEnd");
|
||||
|
||||
b.ToTable("ProductionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Achievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("ActualQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Completion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("PlannedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Risk")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("SampleTime")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "SampleTime")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("ProductionTrendPoints", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("HandlingStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset>("OccurredAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("OrderCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "NodeId");
|
||||
|
||||
b.HasIndex("OrderId", "OccurredAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("RiskEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "ParentNodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionLine", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LineId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "NodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AllowRepeatedWorkOrderCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProductionOrders_Code",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_Code",
|
||||
table: "ProductionOrders",
|
||||
column: "Code");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProductionOrders_Code",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_Code",
|
||||
table: "ProductionOrders",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+648
@@ -0,0 +1,648 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DashboardDbContext))]
|
||||
partial class DashboardDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.20")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("ConsumptionQuota")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("ConsumptionQuotaText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("MaterialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<decimal?>("NetWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("NetWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<decimal?>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("QuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("Unit")
|
||||
.IsRequired()
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("nvarchar(24)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "Category", "MaterialCode");
|
||||
|
||||
b.HasIndex("OrderId", "SourceSheet", "SourceRow")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MaterialQuotas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.Partner", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("ContactPhone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("PartnerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Partners", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProductionLines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("BomQuantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("BomQuantityText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DefectQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DelayReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("MaterialCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("NodeType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("OperationCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("nvarchar(60)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid?>("ParentNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SourceRow")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceSequence")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("SourceSheet")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<string>("Specification")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("SupplyType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<decimal?>("TotalWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("TotalWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<decimal?>("UnitWeight")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("UnitWeightText")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Vendor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("VisualKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "ParentNodeId", "SortOrder");
|
||||
|
||||
b.HasIndex("OrderId", "NodeType", "RiskLevel", "Status");
|
||||
|
||||
b.ToTable("ProductionNodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("BatchQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<int>("CompletedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<double>("DailyDelta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DataSource")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<int>("DelayDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ImportedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<Guid?>("LineId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<double>("PlanAchievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime?>("PlannedEnd")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("PlannedStart")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("RequiredQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SourceDocumentCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("SourceFileHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(260)
|
||||
.HasColumnType("nvarchar(260)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("ThumbnailKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.HasIndex("LineId");
|
||||
|
||||
b.HasIndex("SourceDocumentCode")
|
||||
.IsUnique()
|
||||
.HasFilter("[SourceDocumentCode] <> ''");
|
||||
|
||||
b.HasIndex("Status", "RiskLevel", "PlannedEnd");
|
||||
|
||||
b.ToTable("ProductionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Achievement")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("ActualQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Completion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("PlannedQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Risk")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset>("SampleTime")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "SampleTime")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("ProductionTrendPoints", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("HandlingStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("nvarchar(240)");
|
||||
|
||||
b.Property<DateTimeOffset>("OccurredAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("OrderCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RiskLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId", "NodeId");
|
||||
|
||||
b.HasIndex("OrderId", "OccurredAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("RiskEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.MaterialQuota", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionNode", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "ParentNodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionLine", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LineId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.ProductionTrendPoint", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DongfangHydro.Dashboard.Api.Domain.RiskEvent", b =>
|
||||
{
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionOrder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DongfangHydro.Dashboard.Api.Domain.ProductionNode", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId", "NodeId")
|
||||
.HasPrincipalKey("OrderId", "Id")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [__EFMigrationsHistory] (
|
||||
[MigrationId] nvarchar(150) NOT NULL,
|
||||
[ProductVersion] nvarchar(32) NOT NULL,
|
||||
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [Partners] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[Code] nvarchar(40) NOT NULL,
|
||||
[Name] nvarchar(120) NOT NULL,
|
||||
[PartnerType] nvarchar(32) NOT NULL,
|
||||
[ContactName] nvarchar(80) NOT NULL,
|
||||
[ContactPhone] nvarchar(32) NOT NULL,
|
||||
CONSTRAINT [PK_Partners] PRIMARY KEY ([Id])
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [ProductionLines] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[Code] nvarchar(40) NOT NULL,
|
||||
[Name] nvarchar(80) NOT NULL,
|
||||
[Owner] nvarchar(80) NOT NULL,
|
||||
[IsActive] bit NOT NULL,
|
||||
CONSTRAINT [PK_ProductionLines] PRIMARY KEY ([Id])
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [ProductionOrders] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[Code] nvarchar(40) NOT NULL,
|
||||
[ProductName] nvarchar(200) NOT NULL,
|
||||
[BatchQty] int NOT NULL,
|
||||
[RequiredQty] int NOT NULL,
|
||||
[CompletedQty] int NOT NULL,
|
||||
[Progress] int NOT NULL,
|
||||
[Status] nvarchar(32) NOT NULL,
|
||||
[RiskLevel] nvarchar(32) NOT NULL,
|
||||
[DelayDays] int NOT NULL,
|
||||
[PlannedStart] datetime2 NOT NULL,
|
||||
[PlannedEnd] datetime2 NOT NULL,
|
||||
[LineId] uniqueidentifier NULL,
|
||||
[LineName] nvarchar(80) NOT NULL,
|
||||
[Owner] nvarchar(80) NOT NULL,
|
||||
[PlanAchievement] float NOT NULL,
|
||||
[DailyDelta] float NOT NULL,
|
||||
[ThumbnailKey] nvarchar(40) NOT NULL,
|
||||
[CreatedAt] datetimeoffset NOT NULL,
|
||||
[UpdatedAt] datetimeoffset NOT NULL,
|
||||
[RowVersion] rowversion NOT NULL,
|
||||
CONSTRAINT [PK_ProductionOrders] PRIMARY KEY ([Id]),
|
||||
CONSTRAINT [FK_ProductionOrders_ProductionLines_LineId] FOREIGN KEY ([LineId]) REFERENCES [ProductionLines] ([Id]) ON DELETE SET NULL
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [ProductionNodes] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[OrderId] uniqueidentifier NOT NULL,
|
||||
[ParentNodeId] uniqueidentifier NULL,
|
||||
[Code] nvarchar(60) NOT NULL,
|
||||
[OperationCode] nvarchar(60) NOT NULL,
|
||||
[MaterialCode] nvarchar(80) NOT NULL,
|
||||
[Name] nvarchar(240) NOT NULL,
|
||||
[Level] int NOT NULL,
|
||||
[NodeType] nvarchar(32) NOT NULL,
|
||||
[SupplyType] nvarchar(32) NOT NULL,
|
||||
[RequiredQty] int NOT NULL,
|
||||
[CompletedQty] int NOT NULL,
|
||||
[DefectQty] int NOT NULL,
|
||||
[Progress] int NOT NULL,
|
||||
[Status] nvarchar(32) NOT NULL,
|
||||
[RiskLevel] nvarchar(32) NOT NULL,
|
||||
[DelayDays] int NOT NULL,
|
||||
[DelayReason] nvarchar(500) NOT NULL,
|
||||
[PlannedStart] datetime2 NOT NULL,
|
||||
[PlannedEnd] datetime2 NOT NULL,
|
||||
[ActualStart] datetime2 NULL,
|
||||
[ActualEnd] datetime2 NULL,
|
||||
[Owner] nvarchar(100) NOT NULL,
|
||||
[Vendor] nvarchar(120) NOT NULL,
|
||||
[StationName] nvarchar(120) NOT NULL,
|
||||
[VisualKey] nvarchar(40) NOT NULL,
|
||||
[SortOrder] int NOT NULL,
|
||||
[UpdatedAt] datetimeoffset NOT NULL,
|
||||
[RowVersion] rowversion NOT NULL,
|
||||
CONSTRAINT [PK_ProductionNodes] PRIMARY KEY ([Id]),
|
||||
CONSTRAINT [AK_ProductionNodes_OrderId_Id] UNIQUE ([OrderId], [Id]),
|
||||
CONSTRAINT [FK_ProductionNodes_ProductionNodes_OrderId_ParentNodeId] FOREIGN KEY ([OrderId], [ParentNodeId]) REFERENCES [ProductionNodes] ([OrderId], [Id]) ON DELETE NO ACTION,
|
||||
CONSTRAINT [FK_ProductionNodes_ProductionOrders_OrderId] FOREIGN KEY ([OrderId]) REFERENCES [ProductionOrders] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [ProductionTrendPoints] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[OrderId] uniqueidentifier NOT NULL,
|
||||
[SampleTime] datetimeoffset NOT NULL,
|
||||
[Completion] int NOT NULL,
|
||||
[Risk] int NOT NULL,
|
||||
[PlannedQty] int NOT NULL,
|
||||
[ActualQty] int NOT NULL,
|
||||
[Achievement] float NOT NULL,
|
||||
CONSTRAINT [PK_ProductionTrendPoints] PRIMARY KEY ([Id]),
|
||||
CONSTRAINT [FK_ProductionTrendPoints_ProductionOrders_OrderId] FOREIGN KEY ([OrderId]) REFERENCES [ProductionOrders] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE TABLE [RiskEvents] (
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[OrderId] uniqueidentifier NOT NULL,
|
||||
[NodeId] uniqueidentifier NOT NULL,
|
||||
[OrderCode] nvarchar(40) NOT NULL,
|
||||
[NodeName] nvarchar(240) NOT NULL,
|
||||
[RiskLevel] nvarchar(32) NOT NULL,
|
||||
[Message] nvarchar(500) NOT NULL,
|
||||
[HandlingStatus] nvarchar(32) NOT NULL,
|
||||
[OccurredAt] datetimeoffset NOT NULL,
|
||||
CONSTRAINT [PK_RiskEvents] PRIMARY KEY ([Id]),
|
||||
CONSTRAINT [FK_RiskEvents_ProductionNodes_OrderId_NodeId] FOREIGN KEY ([OrderId], [NodeId]) REFERENCES [ProductionNodes] ([OrderId], [Id]),
|
||||
CONSTRAINT [FK_RiskEvents_ProductionOrders_OrderId] FOREIGN KEY ([OrderId]) REFERENCES [ProductionOrders] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX [IX_Partners_Code] ON [Partners] ([Code]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX [IX_ProductionLines_Code] ON [ProductionLines] ([Code]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_ProductionNodes_OrderId_NodeType_RiskLevel_Status] ON [ProductionNodes] ([OrderId], [NodeType], [RiskLevel], [Status]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_ProductionNodes_OrderId_ParentNodeId_SortOrder] ON [ProductionNodes] ([OrderId], [ParentNodeId], [SortOrder]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX [IX_ProductionOrders_Code] ON [ProductionOrders] ([Code]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_ProductionOrders_LineId] ON [ProductionOrders] ([LineId]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_ProductionOrders_Status_RiskLevel_PlannedEnd] ON [ProductionOrders] ([Status], [RiskLevel], [PlannedEnd]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_ProductionTrendPoints_OrderId_SampleTime] ON [ProductionTrendPoints] ([OrderId], [SampleTime] DESC);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_RiskEvents_OrderId_NodeId] ON [RiskEvents] ([OrderId], [NodeId]);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_RiskEvents_OrderId_OccurredAt] ON [RiskEvents] ([OrderId], [OccurredAt] DESC);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20260710025438_InitialCreate'
|
||||
)
|
||||
BEGIN
|
||||
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
|
||||
VALUES (N'20260710025438_InitialCreate', N'8.0.20');
|
||||
END;
|
||||
GO
|
||||
|
||||
COMMIT;
|
||||
GO
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class MaterialQuota
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrderId { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public string SourceSequence { get; set; } = string.Empty;
|
||||
public string MaterialCode { get; set; } = string.Empty;
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public string Specification { get; set; } = string.Empty;
|
||||
public string Unit { get; set; } = string.Empty;
|
||||
public decimal? Quantity { get; set; }
|
||||
public decimal? NetWeight { get; set; }
|
||||
public decimal? ConsumptionQuota { get; set; }
|
||||
public string QuantityText { get; set; } = string.Empty;
|
||||
public string NetWeightText { get; set; } = string.Empty;
|
||||
public string ConsumptionQuotaText { get; set; } = string.Empty;
|
||||
public string Brand { get; set; } = string.Empty;
|
||||
public string Remark { get; set; } = string.Empty;
|
||||
public string SourceSheet { get; set; } = "材料定额";
|
||||
public int SourceRow { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class Partner
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string PartnerType { get; set; } = "supplier";
|
||||
public string ContactName { get; set; } = string.Empty;
|
||||
public string ContactPhone { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class ProductionLine
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Owner { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class ProductionNode
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrderId { get; set; }
|
||||
public Guid? ParentNodeId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string OperationCode { get; set; } = string.Empty;
|
||||
public string MaterialCode { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SourceSequence { get; set; } = string.Empty;
|
||||
public string Specification { get; set; } = string.Empty;
|
||||
public string Material { get; set; } = string.Empty;
|
||||
public decimal? UnitWeight { get; set; }
|
||||
public decimal? TotalWeight { get; set; }
|
||||
public string UnitWeightText { get; set; } = string.Empty;
|
||||
public string TotalWeightText { get; set; } = string.Empty;
|
||||
public decimal? BomQuantity { get; set; }
|
||||
public string BomQuantityText { get; set; } = string.Empty;
|
||||
public string Remark { get; set; } = string.Empty;
|
||||
public string SourceSheet { get; set; } = string.Empty;
|
||||
public int SourceRow { get; set; }
|
||||
public int Level { get; set; }
|
||||
public string NodeType { get; set; } = "process";
|
||||
public string SupplyType { get; set; } = "self_made";
|
||||
public int RequiredQty { get; set; }
|
||||
public int CompletedQty { get; set; }
|
||||
public int DefectQty { get; set; }
|
||||
public int Progress { get; set; }
|
||||
public string Status { get; set; } = "waiting";
|
||||
public string RiskLevel { get; set; } = "normal";
|
||||
public int DelayDays { get; set; }
|
||||
public string DelayReason { get; set; } = string.Empty;
|
||||
public DateTime? PlannedStart { get; set; }
|
||||
public DateTime? PlannedEnd { get; set; }
|
||||
public DateTime? ActualStart { get; set; }
|
||||
public DateTime? ActualEnd { get; set; }
|
||||
public string Owner { get; set; } = string.Empty;
|
||||
public string Vendor { get; set; } = string.Empty;
|
||||
public string StationName { get; set; } = string.Empty;
|
||||
public string VisualKey { get; set; } = "generic";
|
||||
public int SortOrder { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public byte[] RowVersion { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class ProductionOrder
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public string DataSource { get; set; } = "manual";
|
||||
public string SourceDocumentCode { get; set; } = string.Empty;
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
public string DrawingProductName { get; set; } = string.Empty;
|
||||
public string SourceFileName { get; set; } = string.Empty;
|
||||
public string SourceFileHash { get; set; } = string.Empty;
|
||||
public DateTimeOffset? ImportedAt { get; set; }
|
||||
public int BatchQty { get; set; }
|
||||
public int RequiredQty { get; set; }
|
||||
public int CompletedQty { get; set; }
|
||||
public int Progress { get; set; }
|
||||
public string Status { get; set; } = "waiting";
|
||||
public string RiskLevel { get; set; } = "normal";
|
||||
public int DelayDays { get; set; }
|
||||
public DateTime? PlannedStart { get; set; }
|
||||
public DateTime? PlannedEnd { get; set; }
|
||||
public Guid? LineId { get; set; }
|
||||
public string LineName { get; set; } = string.Empty;
|
||||
public string Owner { get; set; } = string.Empty;
|
||||
public double PlanAchievement { get; set; }
|
||||
public double DailyDelta { get; set; }
|
||||
public string ThumbnailKey { get; set; } = "generic";
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public byte[] RowVersion { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class ProductionTrendPoint
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrderId { get; set; }
|
||||
public DateTimeOffset SampleTime { get; set; }
|
||||
public int Completion { get; set; }
|
||||
public int Risk { get; set; }
|
||||
public int PlannedQty { get; set; }
|
||||
public int ActualQty { get; set; }
|
||||
public double Achievement { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
public sealed class RiskEvent
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrderId { get; set; }
|
||||
public Guid NodeId { get; set; }
|
||||
public string OrderCode { get; set; } = string.Empty;
|
||||
public string NodeName { get; set; } = string.Empty;
|
||||
public string RiskLevel { get; set; } = "normal";
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string HandlingStatus { get; set; } = "processing";
|
||||
public DateTimeOffset OccurredAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>DongfangHydro.Dashboard.Api</RootNamespace>
|
||||
<UserSecretsId>DongfangHydro.Dashboard.Api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.20">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.20" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,117 @@
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Importing;
|
||||
|
||||
public sealed record BomImportCommand(string FilePath, bool PurgeDemo, bool ValidateOnly, bool Force)
|
||||
{
|
||||
public static BomImportCommand? Parse(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || !string.Equals(args[0], "import-bom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string? filePath = null;
|
||||
var purgeDemo = false;
|
||||
var validateOnly = false;
|
||||
var force = false;
|
||||
for (var index = 1; index < args.Length; index++)
|
||||
{
|
||||
switch (args[index])
|
||||
{
|
||||
case "--file":
|
||||
if (index + 1 >= args.Length || args[index + 1].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
throw new BomImportException("import-bom 的 --file 参数必须提供 Excel 路径。");
|
||||
}
|
||||
|
||||
filePath = args[++index];
|
||||
break;
|
||||
case "--purge-demo":
|
||||
purgeDemo = true;
|
||||
break;
|
||||
case "--validate-only":
|
||||
validateOnly = true;
|
||||
break;
|
||||
case "--force":
|
||||
force = true;
|
||||
break;
|
||||
default:
|
||||
throw new BomImportException($"不支持的 import-bom 参数:{args[index]}。");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new BomImportException("import-bom 必须提供 --file <xlsx路径>。");
|
||||
}
|
||||
|
||||
return new BomImportCommand(filePath, purgeDemo, validateOnly, force);
|
||||
}
|
||||
}
|
||||
|
||||
public static class BomImportCommandRunner
|
||||
{
|
||||
public static async Task<object> RunAsync(
|
||||
IServiceProvider services,
|
||||
BomImportCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(command.FilePath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
throw new BomImportException($"Excel 文件不存在:{fullPath}");
|
||||
}
|
||||
|
||||
BomImportDocument document;
|
||||
await using (var stream = File.OpenRead(fullPath))
|
||||
{
|
||||
document = BomWorkbookParser.Parse(stream, Path.GetFileName(fullPath));
|
||||
}
|
||||
|
||||
var levelCounts = document.BomNodes
|
||||
.GroupBy(node => node.Level)
|
||||
.OrderBy(group => group.Key)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
if (command.ValidateOnly)
|
||||
{
|
||||
return new BomValidationReport(
|
||||
fullPath,
|
||||
document.Metadata.SourceDocumentCode,
|
||||
document.Metadata.WorkOrderCode,
|
||||
document.Metadata.SourceFileHash,
|
||||
document.BlockCount,
|
||||
document.BomNodes.Count,
|
||||
document.MaterialQuotas.Count,
|
||||
levelCounts,
|
||||
document.Warnings);
|
||||
}
|
||||
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
|
||||
if (dbContext.Database.IsRelational())
|
||||
{
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return await scope.ServiceProvider.GetRequiredService<BomImportService>()
|
||||
.ImportAsync(document, command.PurgeDemo, cancellationToken, command.Force);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record BomValidationReport(
|
||||
string FilePath,
|
||||
string SourceDocumentCode,
|
||||
string WorkOrderCode,
|
||||
string SourceFileHash,
|
||||
int Blocks,
|
||||
int BomNodes,
|
||||
int MaterialQuotas,
|
||||
IReadOnlyDictionary<int, int> LevelCounts,
|
||||
IReadOnlyList<string> Warnings);
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace DongfangHydro.Dashboard.Api.Importing;
|
||||
|
||||
public sealed record BomImportMetadata(
|
||||
string SourceDocumentCode,
|
||||
string WorkOrderCode,
|
||||
string ProjectName,
|
||||
string ContractProductName,
|
||||
string DrawingProductName,
|
||||
int BatchQuantity,
|
||||
string SourceFileName,
|
||||
string SourceFileHash);
|
||||
|
||||
public sealed record BomImportNode(
|
||||
int SourceRow,
|
||||
int? ParentSourceRow,
|
||||
string SourceSequence,
|
||||
string DrawingNumber,
|
||||
string Name,
|
||||
string Specification,
|
||||
decimal? Quantity,
|
||||
string Material,
|
||||
decimal? UnitWeight,
|
||||
decimal? TotalWeight,
|
||||
string Remark,
|
||||
int Level,
|
||||
string NodeType,
|
||||
string SupplyType,
|
||||
string UnitWeightText = "",
|
||||
string TotalWeightText = "",
|
||||
string QuantityText = "");
|
||||
|
||||
public sealed record MaterialQuotaImportRow(
|
||||
int SourceRow,
|
||||
string Category,
|
||||
string SourceSequence,
|
||||
string MaterialCode,
|
||||
string MaterialName,
|
||||
string Specification,
|
||||
string Unit,
|
||||
decimal? Quantity,
|
||||
decimal? NetWeight,
|
||||
decimal? ConsumptionQuota,
|
||||
string Brand,
|
||||
string Remark,
|
||||
string QuantityText = "",
|
||||
string NetWeightText = "",
|
||||
string ConsumptionQuotaText = "");
|
||||
|
||||
public sealed record BomImportDocument(
|
||||
BomImportMetadata Metadata,
|
||||
IReadOnlyList<BomImportNode> BomNodes,
|
||||
IReadOnlyList<MaterialQuotaImportRow> MaterialQuotas,
|
||||
int BlockCount,
|
||||
IReadOnlyList<string> Warnings);
|
||||
|
||||
public sealed class BomImportException(string message, Exception? innerException = null)
|
||||
: Exception(message, innerException);
|
||||
@@ -0,0 +1,358 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Importing;
|
||||
|
||||
public sealed class BomImportService(DashboardDbContext dbContext)
|
||||
{
|
||||
public async Task<BomImportReport> ImportAsync(
|
||||
BomImportDocument document,
|
||||
bool purgeDemo,
|
||||
CancellationToken cancellationToken,
|
||||
bool forceReplace = false)
|
||||
{
|
||||
IDbContextTransaction? transaction = null;
|
||||
if (dbContext.Database.IsRelational())
|
||||
{
|
||||
transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ordersToRemove = await dbContext.ProductionOrders
|
||||
.Where(order => order.SourceDocumentCode == document.Metadata.SourceDocumentCode
|
||||
|| (purgeDemo && order.DataSource == "demo"))
|
||||
.ToListAsync(cancellationToken);
|
||||
var unchangedOrder = forceReplace ? null : ordersToRemove.SingleOrDefault(order =>
|
||||
order.SourceDocumentCode == document.Metadata.SourceDocumentCode
|
||||
&& order.SourceFileHash == document.Metadata.SourceFileHash);
|
||||
var removedDemoOrders = ordersToRemove.Count(order => order.DataSource == "demo");
|
||||
var replacedOrders = unchangedOrder is null ? ordersToRemove.Count(order =>
|
||||
order.SourceDocumentCode == document.Metadata.SourceDocumentCode
|
||||
&& order.DataSource != "demo") : 0;
|
||||
var removableOrders = unchangedOrder is null
|
||||
? ordersToRemove
|
||||
: ordersToRemove.Where(order => order.DataSource == "demo").ToList();
|
||||
|
||||
if (removableOrders.Count > 0)
|
||||
{
|
||||
await RemoveOrdersAsync(removableOrders, cancellationToken);
|
||||
}
|
||||
|
||||
if (unchangedOrder is not null)
|
||||
{
|
||||
var totalOrders = await dbContext.ProductionOrders.CountAsync(cancellationToken);
|
||||
var productionNodes = await dbContext.ProductionNodes
|
||||
.CountAsync(node => node.OrderId == unchangedOrder.Id, cancellationToken);
|
||||
var materialQuotas = await dbContext.MaterialQuotas
|
||||
.CountAsync(item => item.OrderId == unchangedOrder.Id, cancellationToken);
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return BuildReport(
|
||||
unchangedOrder,
|
||||
document,
|
||||
removedDemoOrders,
|
||||
replacedOrders,
|
||||
totalOrders,
|
||||
productionNodes,
|
||||
materialQuotas,
|
||||
skippedUnchanged: true);
|
||||
}
|
||||
|
||||
var importedAt = DateTimeOffset.UtcNow;
|
||||
var order = BuildOrder(document.Metadata, importedAt);
|
||||
var nodes = BuildNodes(order, document, importedAt);
|
||||
var quotas = BuildMaterialQuotas(order.Id, document);
|
||||
dbContext.ProductionOrders.Add(order);
|
||||
dbContext.ProductionNodes.AddRange(nodes);
|
||||
dbContext.MaterialQuotas.AddRange(quotas);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
var importedTotalOrders = await dbContext.ProductionOrders.CountAsync(cancellationToken);
|
||||
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return BuildReport(
|
||||
order,
|
||||
document,
|
||||
removedDemoOrders,
|
||||
replacedOrders,
|
||||
importedTotalOrders,
|
||||
nodes.Count,
|
||||
quotas.Count,
|
||||
skippedUnchanged: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (transaction is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await transaction.RollbackAsync(CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Preserve the original import failure.
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BomImportReport BuildReport(
|
||||
ProductionOrder order,
|
||||
BomImportDocument document,
|
||||
int removedDemoOrders,
|
||||
int replacedOrders,
|
||||
int totalOrders,
|
||||
int productionNodes,
|
||||
int materialQuotas,
|
||||
bool skippedUnchanged)
|
||||
{
|
||||
return new BomImportReport(
|
||||
order.Id,
|
||||
order.Code,
|
||||
document.Metadata.SourceDocumentCode,
|
||||
document.Metadata.SourceFileHash,
|
||||
removedDemoOrders,
|
||||
replacedOrders,
|
||||
totalOrders,
|
||||
document.BomNodes.Count,
|
||||
productionNodes,
|
||||
materialQuotas,
|
||||
document.BlockCount,
|
||||
document.BomNodes
|
||||
.GroupBy(node => node.Level)
|
||||
.OrderBy(group => group.Key)
|
||||
.ToDictionary(group => group.Key, group => group.Count()),
|
||||
document.Warnings,
|
||||
skippedUnchanged);
|
||||
}
|
||||
|
||||
private async Task RemoveOrdersAsync(
|
||||
IReadOnlyCollection<ProductionOrder> orders,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderIds = orders.Select(order => order.Id).ToArray();
|
||||
var riskEvents = await dbContext.RiskEvents
|
||||
.Where(item => orderIds.Contains(item.OrderId))
|
||||
.ToListAsync(cancellationToken);
|
||||
var trendPoints = await dbContext.ProductionTrendPoints
|
||||
.Where(item => orderIds.Contains(item.OrderId))
|
||||
.ToListAsync(cancellationToken);
|
||||
var quotas = await dbContext.MaterialQuotas
|
||||
.Where(item => orderIds.Contains(item.OrderId))
|
||||
.ToListAsync(cancellationToken);
|
||||
var nodes = await dbContext.ProductionNodes
|
||||
.Where(item => orderIds.Contains(item.OrderId))
|
||||
.OrderByDescending(item => item.Level)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
dbContext.RiskEvents.RemoveRange(riskEvents);
|
||||
dbContext.ProductionTrendPoints.RemoveRange(trendPoints);
|
||||
dbContext.MaterialQuotas.RemoveRange(quotas);
|
||||
dbContext.ProductionNodes.RemoveRange(nodes);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
dbContext.ProductionOrders.RemoveRange(orders);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static ProductionOrder BuildOrder(BomImportMetadata metadata, DateTimeOffset importedAt)
|
||||
{
|
||||
return new ProductionOrder
|
||||
{
|
||||
Id = StableGuid($"{metadata.SourceDocumentCode}|order"),
|
||||
Code = metadata.WorkOrderCode,
|
||||
ProductName = metadata.ContractProductName,
|
||||
DataSource = "excel",
|
||||
SourceDocumentCode = metadata.SourceDocumentCode,
|
||||
ProjectName = metadata.ProjectName,
|
||||
DrawingProductName = metadata.DrawingProductName,
|
||||
SourceFileName = metadata.SourceFileName,
|
||||
SourceFileHash = metadata.SourceFileHash,
|
||||
ImportedAt = importedAt,
|
||||
BatchQty = metadata.BatchQuantity,
|
||||
RequiredQty = metadata.BatchQuantity,
|
||||
CompletedQty = 0,
|
||||
Progress = 0,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
DelayDays = 0,
|
||||
LineName = string.Empty,
|
||||
Owner = string.Empty,
|
||||
PlanAchievement = 0,
|
||||
DailyDelta = 0,
|
||||
ThumbnailKey = "generic",
|
||||
CreatedAt = importedAt,
|
||||
UpdatedAt = importedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ProductionNode> BuildNodes(
|
||||
ProductionOrder order,
|
||||
BomImportDocument document,
|
||||
DateTimeOffset importedAt)
|
||||
{
|
||||
var rootId = StableGuid($"{document.Metadata.SourceDocumentCode}|node|root");
|
||||
var nodeIdsBySourceRow = document.BomNodes.ToDictionary(
|
||||
node => node.SourceRow,
|
||||
node => StableGuid($"{document.Metadata.SourceDocumentCode}|node|明细|{node.SourceRow}"));
|
||||
var nodes = new List<ProductionNode>(document.BomNodes.Count + 1)
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = rootId,
|
||||
OrderId = order.Id,
|
||||
Code = order.Code,
|
||||
MaterialCode = document.Metadata.SourceDocumentCode,
|
||||
Name = order.ProductName,
|
||||
Level = 0,
|
||||
NodeType = "order",
|
||||
SupplyType = "self_made",
|
||||
RequiredQty = order.BatchQty,
|
||||
CompletedQty = 0,
|
||||
Progress = 0,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
SourceSheet = "明细",
|
||||
SourceRow = 0,
|
||||
VisualKey = "generic",
|
||||
SortOrder = 0,
|
||||
UpdatedAt = importedAt,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var source in document.BomNodes)
|
||||
{
|
||||
var parentId = source.ParentSourceRow.HasValue
|
||||
? nodeIdsBySourceRow[source.ParentSourceRow.Value]
|
||||
: rootId;
|
||||
nodes.Add(new ProductionNode
|
||||
{
|
||||
Id = nodeIdsBySourceRow[source.SourceRow],
|
||||
OrderId = order.Id,
|
||||
ParentNodeId = parentId,
|
||||
Code = string.IsNullOrWhiteSpace(source.DrawingNumber)
|
||||
? $"BOM-{source.SourceRow:0000}"
|
||||
: source.DrawingNumber,
|
||||
OperationCode = string.Empty,
|
||||
MaterialCode = source.DrawingNumber,
|
||||
Name = source.Name,
|
||||
SourceSequence = source.SourceSequence,
|
||||
Specification = source.Specification,
|
||||
Material = source.Material,
|
||||
UnitWeight = source.UnitWeight,
|
||||
TotalWeight = source.TotalWeight,
|
||||
UnitWeightText = source.UnitWeightText,
|
||||
TotalWeightText = source.TotalWeightText,
|
||||
BomQuantity = source.Quantity,
|
||||
BomQuantityText = source.QuantityText,
|
||||
Remark = source.Remark,
|
||||
SourceSheet = "明细",
|
||||
SourceRow = source.SourceRow,
|
||||
Level = source.Level,
|
||||
NodeType = source.NodeType,
|
||||
SupplyType = source.SupplyType,
|
||||
RequiredQty = ToOperationalQuantity(source),
|
||||
CompletedQty = 0,
|
||||
DefectQty = 0,
|
||||
Progress = 0,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
DelayDays = 0,
|
||||
DelayReason = string.Empty,
|
||||
Owner = string.Empty,
|
||||
Vendor = string.Empty,
|
||||
StationName = string.Empty,
|
||||
VisualKey = "generic",
|
||||
SortOrder = source.SourceRow,
|
||||
UpdatedAt = importedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static List<MaterialQuota> BuildMaterialQuotas(Guid orderId, BomImportDocument document)
|
||||
{
|
||||
return document.MaterialQuotas.Select(source => new MaterialQuota
|
||||
{
|
||||
Id = StableGuid($"{document.Metadata.SourceDocumentCode}|quota|材料定额|{source.SourceRow}"),
|
||||
OrderId = orderId,
|
||||
Category = source.Category,
|
||||
SourceSequence = source.SourceSequence,
|
||||
MaterialCode = source.MaterialCode,
|
||||
MaterialName = source.MaterialName,
|
||||
Specification = source.Specification,
|
||||
Unit = source.Unit,
|
||||
Quantity = source.Quantity,
|
||||
NetWeight = source.NetWeight,
|
||||
ConsumptionQuota = source.ConsumptionQuota,
|
||||
QuantityText = source.QuantityText,
|
||||
NetWeightText = source.NetWeightText,
|
||||
ConsumptionQuotaText = source.ConsumptionQuotaText,
|
||||
Brand = source.Brand,
|
||||
Remark = source.Remark,
|
||||
SourceSheet = "材料定额",
|
||||
SourceRow = source.SourceRow,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static int ToOperationalQuantity(BomImportNode source)
|
||||
{
|
||||
if (!source.Quantity.HasValue || source.Quantity.Value <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (source.Quantity.Value > int.MaxValue)
|
||||
{
|
||||
throw new BomImportException($"明细第 {source.SourceRow} 行数量超出进度字段可表示范围。");
|
||||
}
|
||||
|
||||
return decimal.ToInt32(decimal.Ceiling(source.Quantity.Value));
|
||||
}
|
||||
|
||||
private static Guid StableGuid(string value)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(value));
|
||||
var bytes = hash[..16];
|
||||
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50);
|
||||
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record BomImportReport(
|
||||
Guid OrderId,
|
||||
string OrderCode,
|
||||
string SourceDocumentCode,
|
||||
string SourceFileHash,
|
||||
int RemovedDemoOrders,
|
||||
int ReplacedOrders,
|
||||
int TotalOrders,
|
||||
int ImportedBomNodes,
|
||||
int TotalProductionNodes,
|
||||
int MaterialQuotas,
|
||||
int Blocks,
|
||||
IReadOnlyDictionary<int, int> LevelCounts,
|
||||
IReadOnlyList<string> Warnings,
|
||||
bool SkippedUnchanged);
|
||||
@@ -0,0 +1,534 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ClosedXML.Excel;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Importing;
|
||||
|
||||
public static partial class BomWorkbookParser
|
||||
{
|
||||
private const string DetailSheetName = "明细";
|
||||
private const string QuotaSheetName = "材料定额";
|
||||
|
||||
public static BomImportDocument Parse(Stream stream, string sourceFileName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
byte[] workbookBytes;
|
||||
using (var buffer = new MemoryStream())
|
||||
{
|
||||
stream.CopyTo(buffer);
|
||||
workbookBytes = buffer.ToArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var workbookStream = new MemoryStream(workbookBytes, writable: false);
|
||||
using var workbook = new XLWorkbook(workbookStream);
|
||||
if (!workbook.TryGetWorksheet(DetailSheetName, out var detailSheet))
|
||||
{
|
||||
throw new BomImportException($"缺少工作表:{DetailSheetName}。");
|
||||
}
|
||||
|
||||
if (!workbook.TryGetWorksheet(QuotaSheetName, out var quotaSheet))
|
||||
{
|
||||
throw new BomImportException($"缺少工作表:{QuotaSheetName}。");
|
||||
}
|
||||
|
||||
var metadata = ParseMetadata(
|
||||
quotaSheet,
|
||||
sourceFileName,
|
||||
Convert.ToHexString(SHA256.HashData(workbookBytes)).ToLowerInvariant());
|
||||
var blockResult = ReadBomBlocks(detailSheet);
|
||||
var nodes = BuildBomNodes(blockResult.Blocks);
|
||||
var quotas = ReadMaterialQuotas(quotaSheet);
|
||||
|
||||
return new BomImportDocument(
|
||||
metadata,
|
||||
nodes,
|
||||
quotas,
|
||||
blockResult.Blocks.Count,
|
||||
blockResult.Warnings);
|
||||
}
|
||||
catch (BomImportException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new BomImportException("Excel BOM 文件无法解析。", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static BomImportMetadata ParseMetadata(
|
||||
IXLWorksheet sheet,
|
||||
string sourceFileName,
|
||||
string sourceFileHash)
|
||||
{
|
||||
var sourceDocumentCode = ExtractValue(sheet.Cell(2, 1).GetFormattedString(), "编号");
|
||||
var projectName = ExtractValue(sheet.Cell(3, 1).GetFormattedString(), "合同(项目)名称");
|
||||
var contractProductName = ExtractValue(sheet.Cell(4, 1).GetFormattedString(), "产品合同名称");
|
||||
var drawingProductName = ExtractValue(sheet.Cell(5, 1).GetFormattedString(), "产品图纸名称");
|
||||
var description = sheet.Cell(6, 1).GetFormattedString();
|
||||
|
||||
var workOrderCode = string.Empty;
|
||||
for (var column = 1; column <= 10; column++)
|
||||
{
|
||||
if (!Normalize(sheet.Cell(3, column).GetFormattedString()).Contains("工作令号", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
workOrderCode = sheet.Cell(3, column + 1).GetFormattedString().Trim();
|
||||
break;
|
||||
}
|
||||
|
||||
var quantityMatch = BatchQuantityRegex().Match(description);
|
||||
var batchQuantity = quantityMatch.Success
|
||||
? int.Parse(quantityMatch.Groups[1].Value, CultureInfo.InvariantCulture)
|
||||
: throw new BomImportException("材料定额说明中未找到产品套数。");
|
||||
|
||||
var required = new Dictionary<string, string>
|
||||
{
|
||||
["定额编号"] = sourceDocumentCode,
|
||||
["工作令号"] = workOrderCode,
|
||||
["项目名称"] = projectName,
|
||||
["产品合同名称"] = contractProductName,
|
||||
["产品图纸名称"] = drawingProductName,
|
||||
};
|
||||
var missing = required.Where(item => string.IsNullOrWhiteSpace(item.Value)).Select(item => item.Key).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
throw new BomImportException($"材料定额缺少元数据:{string.Join("、", missing)}。");
|
||||
}
|
||||
|
||||
return new BomImportMetadata(
|
||||
sourceDocumentCode,
|
||||
workOrderCode,
|
||||
projectName,
|
||||
contractProductName,
|
||||
drawingProductName,
|
||||
batchQuantity,
|
||||
sourceFileName,
|
||||
sourceFileHash);
|
||||
}
|
||||
|
||||
private static BomBlockReadResult ReadBomBlocks(IXLWorksheet sheet)
|
||||
{
|
||||
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||
var blocks = new List<List<RawBomRow>>();
|
||||
var warnings = new List<string>();
|
||||
var current = new List<RawBomRow>();
|
||||
|
||||
for (var rowNumber = 2; rowNumber <= lastRow; rowNumber++)
|
||||
{
|
||||
var row = ReadBomRow(sheet, rowNumber);
|
||||
if (row.IsIgnored)
|
||||
{
|
||||
warnings.Add($"明细第 {rowNumber} 行只有序号,已作为空占位行跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row.IsBlank)
|
||||
{
|
||||
if (current.Count > 0)
|
||||
{
|
||||
blocks.Add(current);
|
||||
current = [];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Add(row);
|
||||
}
|
||||
|
||||
if (current.Count > 0)
|
||||
{
|
||||
blocks.Add(current);
|
||||
}
|
||||
|
||||
if (blocks.Count == 0)
|
||||
{
|
||||
throw new BomImportException("明细工作表没有可导入的 BOM 数据。");
|
||||
}
|
||||
|
||||
return new BomBlockReadResult(blocks, warnings);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<BomImportNode> BuildBomNodes(IReadOnlyList<List<RawBomRow>> blocks)
|
||||
{
|
||||
var builders = new List<NodeBuilder>();
|
||||
foreach (var row in blocks[0])
|
||||
{
|
||||
builders.Add(CreateNode(row, parentSourceRow: null, level: 1));
|
||||
}
|
||||
|
||||
foreach (var block in blocks.Skip(1))
|
||||
{
|
||||
var parentMarker = block[0];
|
||||
var parentKey = BuildBusinessKey(parentMarker.DrawingNumber, parentMarker.Name);
|
||||
var candidates = builders.Where(node => node.BusinessKey == parentKey).ToList();
|
||||
if (candidates.Count != 1)
|
||||
{
|
||||
var reason = candidates.Count == 0 ? "未找到" : $"匹配到 {candidates.Count} 个";
|
||||
throw new BomImportException(
|
||||
$"明细第 {parentMarker.SourceRow} 行父节点 {parentMarker.DrawingNumber} {parentMarker.Name} {reason},无法确定层级。");
|
||||
}
|
||||
|
||||
var parent = candidates[0];
|
||||
foreach (var row in block.Skip(1))
|
||||
{
|
||||
builders.Add(CreateNode(row, parent.SourceRow, parent.Level + 1));
|
||||
}
|
||||
}
|
||||
|
||||
var parentRows = builders
|
||||
.Where(node => node.ParentSourceRow.HasValue)
|
||||
.Select(node => node.ParentSourceRow!.Value)
|
||||
.ToHashSet();
|
||||
|
||||
return builders.Select(node => new BomImportNode(
|
||||
node.SourceRow,
|
||||
node.ParentSourceRow,
|
||||
node.SourceSequence,
|
||||
node.DrawingNumber,
|
||||
node.Name,
|
||||
node.Specification,
|
||||
node.Quantity,
|
||||
node.Material,
|
||||
node.UnitWeight,
|
||||
node.TotalWeight,
|
||||
node.Remark,
|
||||
node.Level,
|
||||
node.Level == 1 ? "part" : parentRows.Contains(node.SourceRow) ? "component" : "material",
|
||||
MapSupplyType(node.Material, parentRows.Contains(node.SourceRow)),
|
||||
node.UnitWeightText,
|
||||
node.TotalWeightText,
|
||||
node.QuantityText)).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MaterialQuotaImportRow> ReadMaterialQuotas(IXLWorksheet sheet)
|
||||
{
|
||||
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 7;
|
||||
var category = string.Empty;
|
||||
var rows = new List<MaterialQuotaImportRow>();
|
||||
|
||||
for (var rowNumber = 8; rowNumber <= lastRow; rowNumber++)
|
||||
{
|
||||
var sourceSequence = sheet.Cell(rowNumber, 1).GetFormattedString().Trim();
|
||||
var materialCode = sheet.Cell(rowNumber, 2).GetFormattedString().Trim();
|
||||
var materialName = sheet.Cell(rowNumber, 3).GetFormattedString().Trim();
|
||||
var trailingValues = Enumerable.Range(4, 7)
|
||||
.Select(column => sheet.Cell(rowNumber, column).GetFormattedString().Trim())
|
||||
.ToArray();
|
||||
if (!string.IsNullOrWhiteSpace(materialCode) && string.IsNullOrWhiteSpace(materialName))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourceSequence) || trailingValues.Any(value => !string.IsNullOrWhiteSpace(value)))
|
||||
{
|
||||
throw new BomImportException($"材料定额第 {rowNumber} 行缺少物料名称。");
|
||||
}
|
||||
|
||||
category = materialCode;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(materialCode) && string.IsNullOrWhiteSpace(materialName))
|
||||
{
|
||||
if (IsMaterialQuotaFooter(sourceSequence, trailingValues))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sourceSequence)
|
||||
|| trailingValues.Any(value => !string.IsNullOrWhiteSpace(value)))
|
||||
{
|
||||
throw new BomImportException($"材料定额第 {rowNumber} 行缺少物料编码和物料名称。");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(materialName))
|
||||
{
|
||||
throw new BomImportException($"材料定额第 {rowNumber} 行缺少物料名称。");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(category))
|
||||
{
|
||||
throw new BomImportException($"材料定额第 {rowNumber} 行缺少所属分类。");
|
||||
}
|
||||
|
||||
rows.Add(new MaterialQuotaImportRow(
|
||||
rowNumber,
|
||||
category,
|
||||
sourceSequence,
|
||||
materialCode,
|
||||
materialName,
|
||||
sheet.Cell(rowNumber, 4).GetFormattedString().Trim(),
|
||||
sheet.Cell(rowNumber, 5).GetFormattedString().Trim(),
|
||||
ReadQuotaDecimal(sheet.Cell(rowNumber, 6)),
|
||||
ReadQuotaDecimal(sheet.Cell(rowNumber, 7)),
|
||||
ReadQuotaDecimal(sheet.Cell(rowNumber, 8)),
|
||||
sheet.Cell(rowNumber, 9).GetFormattedString().Trim(),
|
||||
sheet.Cell(rowNumber, 10).GetFormattedString().Trim(),
|
||||
sheet.Cell(rowNumber, 6).GetFormattedString().Trim(),
|
||||
sheet.Cell(rowNumber, 7).GetFormattedString().Trim(),
|
||||
sheet.Cell(rowNumber, 8).GetFormattedString().Trim()));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static bool IsMaterialQuotaFooter(string sourceSequence, IReadOnlyCollection<string> trailingValues)
|
||||
{
|
||||
if (trailingValues.Any(value => !string.IsNullOrWhiteSpace(value)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = Normalize(sourceSequence);
|
||||
return normalized.Contains("编码:", StringComparison.Ordinal)
|
||||
&& normalized.Contains("编制:", StringComparison.Ordinal)
|
||||
&& normalized.Contains("审核:", StringComparison.Ordinal)
|
||||
&& normalized.Contains("日期:", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static RawBomRow ReadBomRow(IXLWorksheet sheet, int rowNumber)
|
||||
{
|
||||
var values = Enumerable.Range(1, 9)
|
||||
.Select(column => sheet.Cell(rowNumber, column).GetFormattedString().Trim())
|
||||
.ToArray();
|
||||
if (values.All(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
return RawBomRow.Blank(rowNumber);
|
||||
}
|
||||
|
||||
var name = values[2];
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(values[0]) && values.Skip(1).All(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
return RawBomRow.Ignored(rowNumber, values[0]);
|
||||
}
|
||||
|
||||
throw new BomImportException($"明细第 {rowNumber} 行缺少名称。");
|
||||
}
|
||||
|
||||
var quantityValue = ReadBomQuantity(sheet.Cell(rowNumber, 5));
|
||||
return new RawBomRow(
|
||||
rowNumber,
|
||||
values[0],
|
||||
values[1],
|
||||
name,
|
||||
values[3],
|
||||
quantityValue,
|
||||
values[4],
|
||||
values[5],
|
||||
ReadBomWeight(sheet.Cell(rowNumber, 7)),
|
||||
ReadBomWeight(sheet.Cell(rowNumber, 8)),
|
||||
values[6],
|
||||
values[7],
|
||||
values[8],
|
||||
false,
|
||||
false);
|
||||
}
|
||||
|
||||
private static NodeBuilder CreateNode(RawBomRow row, int? parentSourceRow, int level)
|
||||
{
|
||||
return new NodeBuilder(
|
||||
row.SourceRow,
|
||||
parentSourceRow,
|
||||
row.SourceSequence,
|
||||
row.DrawingNumber,
|
||||
row.Name,
|
||||
row.Specification,
|
||||
row.Quantity,
|
||||
row.QuantityText,
|
||||
row.Material,
|
||||
row.UnitWeight,
|
||||
row.TotalWeight,
|
||||
row.UnitWeightText,
|
||||
row.TotalWeightText,
|
||||
row.Remark,
|
||||
level,
|
||||
BuildBusinessKey(row.DrawingNumber, row.Name));
|
||||
}
|
||||
|
||||
private static string MapSupplyType(string material, bool hasChildren)
|
||||
{
|
||||
var normalized = Normalize(material);
|
||||
if (normalized.Contains("外协", StringComparison.Ordinal))
|
||||
{
|
||||
return "outsourced";
|
||||
}
|
||||
|
||||
if (normalized.Contains("部件", StringComparison.Ordinal)
|
||||
|| normalized.Contains("装配", StringComparison.Ordinal)
|
||||
|| normalized.Contains("装焊", StringComparison.Ordinal)
|
||||
|| normalized.Contains("焊接", StringComparison.Ordinal)
|
||||
|| normalized.Contains("加工", StringComparison.Ordinal))
|
||||
{
|
||||
return "self_made";
|
||||
}
|
||||
|
||||
if (normalized.Contains("成品", StringComparison.Ordinal)
|
||||
|| normalized.Contains("外购", StringComparison.Ordinal)
|
||||
|| normalized.Contains("标准件", StringComparison.Ordinal))
|
||||
{
|
||||
return "purchased";
|
||||
}
|
||||
|
||||
return hasChildren ? "self_made" : "purchased";
|
||||
}
|
||||
|
||||
private static decimal? ReadBomQuantity(IXLCell cell)
|
||||
{
|
||||
if (cell.IsEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cell.TryGetValue<decimal>(out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var text = cell.GetFormattedString().Trim();
|
||||
return decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out value)
|
||||
|| decimal.TryParse(text, NumberStyles.Number, CultureInfo.GetCultureInfo("zh-CN"), out value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private static decimal? ReadBomWeight(IXLCell cell)
|
||||
{
|
||||
if (cell.IsEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cell.TryGetValue<decimal>(out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var text = cell.GetFormattedString().Trim();
|
||||
return decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out value)
|
||||
|| decimal.TryParse(text, NumberStyles.Number, CultureInfo.GetCultureInfo("zh-CN"), out value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private static decimal? ReadQuotaDecimal(IXLCell cell)
|
||||
{
|
||||
if (cell.IsEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cell.TryGetValue<decimal>(out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var text = cell.GetFormattedString().Trim();
|
||||
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out value)
|
||||
|| decimal.TryParse(text, NumberStyles.Number, CultureInfo.GetCultureInfo("zh-CN"), out value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var match = NumberWithUnitRegex().Match(text);
|
||||
return match.Success
|
||||
&& decimal.TryParse(match.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string ExtractValue(string text, string label)
|
||||
{
|
||||
var normalized = text.Trim();
|
||||
var separatorIndex = normalized.IndexOf(':');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
separatorIndex = normalized.IndexOf(':');
|
||||
}
|
||||
|
||||
if (separatorIndex < 0 || !normalized[..separatorIndex].Contains(label, StringComparison.Ordinal))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return normalized[(separatorIndex + 1)..].Trim();
|
||||
}
|
||||
|
||||
private static string BuildBusinessKey(string drawingNumber, string name)
|
||||
{
|
||||
return $"{Normalize(drawingNumber)}|{Normalize(name)}";
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
return string.Concat(value.Where(character => !char.IsWhiteSpace(character))).ToUpperInvariant();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(\d+)\s*套", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex BatchQuantityRegex();
|
||||
|
||||
[GeneratedRegex(@"^\s*(-?\d+(?:\.\d+)?)\s*[^\d\s].*$", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex NumberWithUnitRegex();
|
||||
|
||||
private sealed record RawBomRow(
|
||||
int SourceRow,
|
||||
string SourceSequence,
|
||||
string DrawingNumber,
|
||||
string Name,
|
||||
string Specification,
|
||||
decimal? Quantity,
|
||||
string QuantityText,
|
||||
string Material,
|
||||
decimal? UnitWeight,
|
||||
decimal? TotalWeight,
|
||||
string UnitWeightText,
|
||||
string TotalWeightText,
|
||||
string Remark,
|
||||
bool IsBlank,
|
||||
bool IsIgnored)
|
||||
{
|
||||
public static RawBomRow Blank(int sourceRow)
|
||||
{
|
||||
return new RawBomRow(sourceRow, string.Empty, string.Empty, string.Empty, string.Empty, null,
|
||||
string.Empty, string.Empty, null, null, string.Empty, string.Empty, string.Empty, true, false);
|
||||
}
|
||||
|
||||
public static RawBomRow Ignored(int sourceRow, string sourceSequence)
|
||||
{
|
||||
return new RawBomRow(sourceRow, sourceSequence, string.Empty, string.Empty, string.Empty, null,
|
||||
string.Empty, string.Empty, null, null, string.Empty, string.Empty, string.Empty, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record BomBlockReadResult(
|
||||
List<List<RawBomRow>> Blocks,
|
||||
IReadOnlyList<string> Warnings);
|
||||
|
||||
private sealed record NodeBuilder(
|
||||
int SourceRow,
|
||||
int? ParentSourceRow,
|
||||
string SourceSequence,
|
||||
string DrawingNumber,
|
||||
string Name,
|
||||
string Specification,
|
||||
decimal? Quantity,
|
||||
string QuantityText,
|
||||
string Material,
|
||||
decimal? UnitWeight,
|
||||
decimal? TotalWeight,
|
||||
string UnitWeightText,
|
||||
string TotalWeightText,
|
||||
string Remark,
|
||||
int Level,
|
||||
string BusinessKey);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Importing;
|
||||
using DongfangHydro.Dashboard.Api.Realtime;
|
||||
using DongfangHydro.Dashboard.Api.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
var importCommand = BomImportCommand.Parse(args);
|
||||
var builder = WebApplication.CreateBuilder(importCommand is null ? args : []);
|
||||
if (importCommand is not null)
|
||||
{
|
||||
builder.Configuration.AddUserSecrets<Program>(optional: true);
|
||||
}
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DashboardDb")
|
||||
?? throw new InvalidOperationException("Connection string 'DashboardDb' is not configured.");
|
||||
|
||||
builder.Services.AddDbContext<DashboardDbContext>(options => options.UseSqlServer(connectionString));
|
||||
builder.Services.AddScoped<ProgressAggregationService>();
|
||||
builder.Services.AddScoped<DashboardSeeder>();
|
||||
builder.Services.AddScoped<DashboardQueryService>();
|
||||
builder.Services.AddScoped<ProgressUpdateService>();
|
||||
builder.Services.AddScoped<BomImportService>();
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSingleton<IDashboardNotifier, SignalRDashboardNotifier>();
|
||||
builder.Services.AddSingleton<DashboardUpdateQueue>();
|
||||
builder.Services.AddSingleton<IDashboardUpdateQueue>(services =>
|
||||
services.GetRequiredService<DashboardUpdateQueue>());
|
||||
builder.Services.AddHostedService(services => services.GetRequiredService<DashboardUpdateQueue>());
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
|
||||
if (origins.Length == 0)
|
||||
{
|
||||
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
|
||||
return;
|
||||
}
|
||||
|
||||
policy.WithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
if (importCommand is not null)
|
||||
{
|
||||
var report = await BomImportCommandRunner.RunAsync(app.Services, importCommand, CancellationToken.None);
|
||||
Console.WriteLine(JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return;
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
app.MapGet("/api/dashboard/overview", async (
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetOverviewAsync(cancellationToken)));
|
||||
|
||||
app.MapGet("/api/orders", async (
|
||||
string? keyword,
|
||||
string? status,
|
||||
Guid? lineId,
|
||||
int? page,
|
||||
int? pageSize,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetOrdersAsync(
|
||||
keyword,
|
||||
status,
|
||||
lineId,
|
||||
page ?? 1,
|
||||
pageSize ?? 20,
|
||||
cancellationToken)));
|
||||
|
||||
app.MapGet("/api/orders/{orderId:guid}", async (
|
||||
Guid orderId,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var order = await service.GetOrderAsync(orderId, cancellationToken);
|
||||
return order is null ? Results.NotFound() : Results.Ok(order);
|
||||
});
|
||||
|
||||
app.MapGet("/api/orders/{orderId:guid}/tree", async (
|
||||
Guid orderId,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tree = await service.GetTreeAsync(orderId, cancellationToken);
|
||||
return tree is null ? Results.NotFound() : Results.Ok(tree);
|
||||
});
|
||||
|
||||
app.MapGet("/api/orders/{orderId:guid}/trend", async (
|
||||
Guid orderId,
|
||||
int? limit,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetTrendAsync(orderId, limit ?? 12, cancellationToken)));
|
||||
|
||||
app.MapGet("/api/orders/{orderId:guid}/material-quotas", async (
|
||||
Guid orderId,
|
||||
string? category,
|
||||
string? keyword,
|
||||
int? page,
|
||||
int? pageSize,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetMaterialQuotasAsync(
|
||||
orderId,
|
||||
category,
|
||||
keyword,
|
||||
page ?? 1,
|
||||
pageSize ?? 50,
|
||||
cancellationToken)));
|
||||
|
||||
app.MapGet("/api/risk-events", async (
|
||||
Guid? orderId,
|
||||
int? limit,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetRiskEventsAsync(orderId, limit ?? 20, cancellationToken)));
|
||||
|
||||
app.MapGet("/api/delay-top", async (
|
||||
int? limit,
|
||||
DashboardQueryService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
Results.Ok(await service.GetDelayTopAsync(limit ?? 5, cancellationToken)));
|
||||
|
||||
app.MapPatch("/api/nodes/{nodeId:guid}/progress", async (
|
||||
Guid nodeId,
|
||||
UpdateNodeProgressRequest request,
|
||||
ProgressUpdateService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateAsync(nodeId, request, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
return Results.ValidationProblem(new Dictionary<string, string[]>
|
||||
{
|
||||
["progress"] = [exception.Message],
|
||||
});
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return Results.Conflict(new
|
||||
{
|
||||
code = "progress_conflict",
|
||||
message = "The production node changed after it was loaded. Reload the node and retry.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.MapHub<DashboardHub>("/hubs/dashboard");
|
||||
await DatabaseInitializer.InitializeAsync(app);
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
@@ -0,0 +1,5 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class DashboardHub : Hub;
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Concurrent;
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class DashboardUpdateQueue(
|
||||
IDashboardNotifier notifier,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<DashboardUpdateQueue> logger)
|
||||
: BackgroundService, IDashboardUpdateQueue
|
||||
{
|
||||
private const int MaximumAttempts = 5;
|
||||
private readonly ConcurrentDictionary<Guid, QueuedUpdate> pendingByOrder = new();
|
||||
private readonly SemaphoreSlim signal = new(0, 1);
|
||||
private int signalScheduled;
|
||||
|
||||
public ValueTask EnqueueAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
pendingByOrder.AddOrUpdate(
|
||||
update.Order.Id,
|
||||
_ => new QueuedUpdate(update),
|
||||
(_, _) => new QueuedUpdate(update));
|
||||
ScheduleWork();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await signal.WaitAsync(stoppingToken);
|
||||
Interlocked.Exchange(ref signalScheduled, 0);
|
||||
|
||||
foreach (var orderId in pendingByOrder.Keys)
|
||||
{
|
||||
if (pendingByOrder.TryRemove(orderId, out var item))
|
||||
{
|
||||
await PublishLatestAsync(orderId, item, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingByOrder.IsEmpty)
|
||||
{
|
||||
ScheduleWork();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishLatestAsync(
|
||||
Guid orderId,
|
||||
QueuedUpdate initial,
|
||||
CancellationToken stoppingToken)
|
||||
{
|
||||
var current = initial;
|
||||
var attempt = 1;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (pendingByOrder.TryRemove(orderId, out var newer))
|
||||
{
|
||||
current = newer;
|
||||
attempt = 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var overview = await scope.ServiceProvider
|
||||
.GetRequiredService<DashboardQueryService>()
|
||||
.GetOverviewAsync(stoppingToken);
|
||||
await notifier.PublishUpdateAsync(current.Update, overview, stoppingToken);
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(
|
||||
exception,
|
||||
"Dashboard update broadcast failed on attempt {Attempt} for order {OrderId}.",
|
||||
attempt,
|
||||
orderId);
|
||||
|
||||
if (attempt >= MaximumAttempts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingByOrder.ContainsKey(orderId))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(attempt * 2), stoppingToken);
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleWork()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref signalScheduled, 1, 0) == 0)
|
||||
{
|
||||
signal.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record QueuedUpdate(UpdateNodeProgressResultDto Update);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public interface IDashboardNotifier
|
||||
{
|
||||
Task PublishUpdateAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
DashboardOverviewDto overview,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public interface IDashboardUpdateQueue
|
||||
{
|
||||
ValueTask EnqueueAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class SignalRDashboardNotifier(IHubContext<DashboardHub> hubContext)
|
||||
: IDashboardNotifier
|
||||
{
|
||||
public async Task PublishUpdateAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
DashboardOverviewDto overview,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var clients = hubContext.Clients.All;
|
||||
await Task.WhenAll(
|
||||
clients.SendAsync("nodeUpdated", update.Node, cancellationToken),
|
||||
clients.SendAsync("orderUpdated", update.Order, cancellationToken),
|
||||
clients.SendAsync("trendAppended", update.TrendPoint, cancellationToken),
|
||||
clients.SendAsync("overviewUpdated", overview, cancellationToken),
|
||||
update.RiskEvent is null
|
||||
? Task.CompletedTask
|
||||
: clients.SendAsync("riskEventCreated", update.RiskEvent, cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
public static class DashboardMapper
|
||||
{
|
||||
public static ProductionNodeDto? BuildTree(IReadOnlyCollection<ProductionNode> nodes)
|
||||
{
|
||||
var root = nodes
|
||||
.Where(node => node.ParentNodeId is null)
|
||||
.OrderBy(node => node.SortOrder)
|
||||
.FirstOrDefault(node => node.NodeType == "order")
|
||||
?? nodes.Where(node => node.ParentNodeId is null).OrderBy(node => node.SortOrder).FirstOrDefault();
|
||||
|
||||
if (root is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var childrenByParent = nodes
|
||||
.Where(node => node.ParentNodeId.HasValue)
|
||||
.GroupBy(node => node.ParentNodeId!.Value)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.OrderBy(node => node.SortOrder).ThenBy(node => node.Code).ToList());
|
||||
|
||||
return MapNode(root, childrenByParent, []);
|
||||
}
|
||||
|
||||
public static ProductionNodeDto MapNode(ProductionNode node)
|
||||
{
|
||||
return MapNode(node, new Dictionary<Guid, List<ProductionNode>>(), []);
|
||||
}
|
||||
|
||||
public static TrendPointDto MapTrend(ProductionTrendPoint point)
|
||||
{
|
||||
return new TrendPointDto(
|
||||
point.SampleTime.ToLocalTime().ToString("HH:mm:ss"),
|
||||
point.Completion,
|
||||
point.Risk,
|
||||
point.PlannedQty,
|
||||
point.ActualQty,
|
||||
Math.Round(point.Achievement, 1));
|
||||
}
|
||||
|
||||
public static RiskEventDto MapRiskEvent(RiskEvent riskEvent)
|
||||
{
|
||||
return new RiskEventDto(
|
||||
riskEvent.Id,
|
||||
riskEvent.OrderId,
|
||||
riskEvent.NodeId,
|
||||
riskEvent.OrderCode,
|
||||
riskEvent.NodeName,
|
||||
riskEvent.RiskLevel,
|
||||
riskEvent.Message,
|
||||
riskEvent.OccurredAt.ToLocalTime().ToString("HH:mm:ss"),
|
||||
riskEvent.HandlingStatus);
|
||||
}
|
||||
|
||||
public static OrderSummaryDto MapOrder(
|
||||
ProductionOrder order,
|
||||
ProductionNodeDto root,
|
||||
IReadOnlyList<TrendPointDto> trend,
|
||||
IReadOnlyList<RiskEventDto> events)
|
||||
{
|
||||
return new OrderSummaryDto(
|
||||
order.Id,
|
||||
order.Code,
|
||||
order.ProductName,
|
||||
order.BatchQty,
|
||||
order.RequiredQty,
|
||||
order.CompletedQty,
|
||||
order.Progress,
|
||||
order.Status,
|
||||
order.RiskLevel,
|
||||
order.DelayDays,
|
||||
FormatDate(order.PlannedStart),
|
||||
FormatDate(order.PlannedEnd),
|
||||
order.LineName,
|
||||
order.Owner,
|
||||
Math.Round(order.PlanAchievement, 1),
|
||||
Math.Round(order.DailyDelta, 1),
|
||||
order.ThumbnailKey,
|
||||
root,
|
||||
trend,
|
||||
events);
|
||||
}
|
||||
|
||||
private static ProductionNodeDto MapNode(
|
||||
ProductionNode node,
|
||||
IReadOnlyDictionary<Guid, List<ProductionNode>> childrenByParent,
|
||||
HashSet<Guid> ancestors)
|
||||
{
|
||||
if (!ancestors.Add(node.Id))
|
||||
{
|
||||
throw new InvalidOperationException($"Production node cycle detected at {node.Id}.");
|
||||
}
|
||||
|
||||
var children = childrenByParent.TryGetValue(node.Id, out var childNodes)
|
||||
? childNodes.Select(child => MapNode(child, childrenByParent, new HashSet<Guid>(ancestors))).ToList()
|
||||
: [];
|
||||
|
||||
return new ProductionNodeDto(
|
||||
node.Id,
|
||||
node.OrderId,
|
||||
node.ParentNodeId,
|
||||
node.Code,
|
||||
node.OperationCode,
|
||||
node.MaterialCode,
|
||||
node.Name,
|
||||
node.SourceSequence,
|
||||
node.Specification,
|
||||
node.Material,
|
||||
node.UnitWeight,
|
||||
node.TotalWeight,
|
||||
node.UnitWeightText,
|
||||
node.TotalWeightText,
|
||||
node.BomQuantity,
|
||||
node.BomQuantityText,
|
||||
node.Remark,
|
||||
node.SourceSheet,
|
||||
node.SourceRow,
|
||||
node.Level,
|
||||
node.NodeType,
|
||||
node.SupplyType,
|
||||
node.RequiredQty,
|
||||
node.CompletedQty,
|
||||
node.DefectQty,
|
||||
node.Progress,
|
||||
node.Status,
|
||||
node.RiskLevel,
|
||||
node.DelayDays,
|
||||
node.DelayReason,
|
||||
FormatDate(node.PlannedStart),
|
||||
FormatDate(node.PlannedEnd),
|
||||
FormatDate(node.ActualStart),
|
||||
FormatDate(node.ActualEnd),
|
||||
node.Owner,
|
||||
node.Vendor,
|
||||
node.StationName,
|
||||
node.VisualKey,
|
||||
Convert.ToBase64String(node.RowVersion),
|
||||
children);
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTime? value)
|
||||
{
|
||||
return value.HasValue ? value.Value.ToString("yyyy-MM-dd") : string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
public sealed class DashboardQueryService(DashboardDbContext dbContext)
|
||||
{
|
||||
public async Task<PagedResult<MaterialQuotaDto>> GetMaterialQuotasAsync(
|
||||
Guid orderId,
|
||||
string? category,
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 200);
|
||||
var query = dbContext.MaterialQuotas.AsNoTracking().Where(item => item.OrderId == orderId);
|
||||
if (!string.IsNullOrWhiteSpace(category))
|
||||
{
|
||||
var normalizedCategory = category.Trim();
|
||||
query = query.Where(item => item.Category == normalizedCategory);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var normalizedKeyword = keyword.Trim();
|
||||
query = query.Where(item =>
|
||||
item.MaterialCode.Contains(normalizedKeyword)
|
||||
|| item.MaterialName.Contains(normalizedKeyword)
|
||||
|| item.Specification.Contains(normalizedKeyword));
|
||||
}
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var rows = await query
|
||||
.OrderBy(item => item.Category)
|
||||
.ThenBy(item => item.SourceRow)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
var items = rows.Select(item => new MaterialQuotaDto(
|
||||
item.Id,
|
||||
item.OrderId,
|
||||
item.Category,
|
||||
item.SourceSequence,
|
||||
item.MaterialCode,
|
||||
item.MaterialName,
|
||||
item.Specification,
|
||||
item.Unit,
|
||||
item.Quantity,
|
||||
item.NetWeight,
|
||||
item.ConsumptionQuota,
|
||||
item.QuantityText,
|
||||
item.NetWeightText,
|
||||
item.ConsumptionQuotaText,
|
||||
item.Brand,
|
||||
item.Remark,
|
||||
item.SourceSheet,
|
||||
item.SourceRow)).ToList();
|
||||
|
||||
return new PagedResult<MaterialQuotaDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<OrderSummaryDto>> GetOrdersAsync(
|
||||
string? keyword,
|
||||
string? status,
|
||||
Guid? lineId,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var query = dbContext.ProductionOrders.AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var normalized = keyword.Trim();
|
||||
query = query.Where(order =>
|
||||
order.Code.Contains(normalized)
|
||||
|| order.ProductName.Contains(normalized)
|
||||
|| order.LineName.Contains(normalized)
|
||||
|| order.Owner.Contains(normalized));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
query = query.Where(order => order.Status == status);
|
||||
}
|
||||
|
||||
if (lineId.HasValue)
|
||||
{
|
||||
query = query.Where(order => order.LineId == lineId);
|
||||
}
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var orders = await query
|
||||
.OrderBy(order => order.Code)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
var items = await BuildOrderSummariesAsync(orders, cancellationToken);
|
||||
|
||||
return new PagedResult<OrderSummaryDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<OrderSummaryDto?> GetOrderAsync(Guid orderId, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await dbContext.ProductionOrders
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(item => item.Id == orderId, cancellationToken);
|
||||
if (order is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await BuildOrderSummariesAsync([order], cancellationToken)).SingleOrDefault();
|
||||
}
|
||||
|
||||
public async Task<ProductionNodeDto?> GetTreeAsync(Guid orderId, CancellationToken cancellationToken)
|
||||
{
|
||||
var nodes = await dbContext.ProductionNodes
|
||||
.AsNoTracking()
|
||||
.Where(node => node.OrderId == orderId)
|
||||
.OrderBy(node => node.Level)
|
||||
.ThenBy(node => node.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return DashboardMapper.BuildTree(nodes);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TrendPointDto>> GetTrendAsync(
|
||||
Guid orderId,
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var points = await dbContext.ProductionTrendPoints
|
||||
.AsNoTracking()
|
||||
.Where(point => point.OrderId == orderId)
|
||||
.OrderByDescending(point => point.SampleTime)
|
||||
.Take(Math.Clamp(limit, 1, 100))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return points.OrderBy(point => point.SampleTime).Select(DashboardMapper.MapTrend).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RiskEventDto>> GetRiskEventsAsync(
|
||||
Guid? orderId,
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.RiskEvents.AsNoTracking();
|
||||
if (orderId.HasValue)
|
||||
{
|
||||
query = query.Where(riskEvent => riskEvent.OrderId == orderId);
|
||||
}
|
||||
|
||||
var events = await query
|
||||
.OrderByDescending(riskEvent => riskEvent.OccurredAt)
|
||||
.Take(Math.Clamp(limit, 1, 100))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return events.Select(DashboardMapper.MapRiskEvent).ToList();
|
||||
}
|
||||
|
||||
public async Task<DashboardOverviewDto> GetOverviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var orders = await dbContext.ProductionOrders.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var totalRequired = orders.Sum(order => order.RequiredQty);
|
||||
var totalCompleted = orders.Sum(order => order.CompletedQty);
|
||||
|
||||
return new DashboardOverviewDto(
|
||||
orders.Count,
|
||||
totalRequired,
|
||||
totalCompleted,
|
||||
totalRequired == 0 ? 0 : Math.Round(totalCompleted * 100d / totalRequired, 1),
|
||||
orders.Count(order => order.DelayDays > 0 || order.Status is "delayed" or "blocked"),
|
||||
orders.Count(order => order.RiskLevel == "critical"),
|
||||
orders.Count(order => order.RiskLevel == "warning"),
|
||||
orders.Count == 0 ? 0 : Math.Round(orders.Average(order => order.PlanAchievement), 1),
|
||||
DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DelayTopItemDto>> GetDelayTopAsync(
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orders = await dbContext.ProductionOrders
|
||||
.AsNoTracking()
|
||||
.Where(order => order.DelayDays > 0 || order.Status == "delayed" || order.Status == "blocked")
|
||||
.OrderByDescending(order => order.DelayDays)
|
||||
.ThenByDescending(order => order.RiskLevel == "critical")
|
||||
.Take(Math.Clamp(limit, 1, 50))
|
||||
.ToListAsync(cancellationToken);
|
||||
var orderIds = orders.Select(order => order.Id).ToArray();
|
||||
var recentEvents = await LoadRecentRiskEventsAsync(orderIds, 1, cancellationToken);
|
||||
var result = new List<DelayTopItemDto>(orders.Count);
|
||||
for (var index = 0; index < orders.Count; index++)
|
||||
{
|
||||
var order = orders[index];
|
||||
var delayReason = recentEvents.FirstOrDefault(riskEvent => riskEvent.OrderId == order.Id)?.Message;
|
||||
result.Add(new DelayTopItemDto(
|
||||
index + 1,
|
||||
order.Id,
|
||||
order.Code,
|
||||
order.ProductName,
|
||||
order.DelayDays,
|
||||
delayReason ?? "计划节拍低于目标",
|
||||
order.RiskLevel));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<OrderSummaryDto>> BuildOrderSummariesAsync(
|
||||
IReadOnlyList<ProductionOrder> orders,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (orders.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var orderIds = orders.Select(order => order.Id).ToList();
|
||||
var nodes = await dbContext.ProductionNodes
|
||||
.AsNoTracking()
|
||||
.Where(node => orderIds.Contains(node.OrderId))
|
||||
.OrderBy(node => node.Level)
|
||||
.ThenBy(node => node.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
var trend = await LoadRecentTrendAsync(orderIds, 12, cancellationToken);
|
||||
var events = await LoadRecentRiskEventsAsync(orderIds, 8, cancellationToken);
|
||||
var result = new List<OrderSummaryDto>(orders.Count);
|
||||
foreach (var order in orders)
|
||||
{
|
||||
var root = DashboardMapper.BuildTree(nodes.Where(node => node.OrderId == order.Id).ToList());
|
||||
if (root is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var orderTrend = trend.Where(point => point.OrderId == order.Id);
|
||||
var orderEvents = events
|
||||
.Where(riskEvent => riskEvent.OrderId == order.Id)
|
||||
.OrderByDescending(riskEvent => riskEvent.OccurredAt);
|
||||
|
||||
result.Add(DashboardMapper.MapOrder(
|
||||
order,
|
||||
root,
|
||||
orderTrend.OrderBy(point => point.SampleTime).Select(DashboardMapper.MapTrend).ToList(),
|
||||
orderEvents.Select(DashboardMapper.MapRiskEvent).ToList()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<ProductionTrendPoint>> LoadRecentTrendAsync(
|
||||
IReadOnlyCollection<Guid> orderIds,
|
||||
int perOrderLimit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (dbContext.Database.IsSqlServer())
|
||||
{
|
||||
return await BuildRecentTrendQuery(dbContext, orderIds, perOrderLimit)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var points = await dbContext.ProductionTrendPoints
|
||||
.AsNoTracking()
|
||||
.Where(point => orderIds.Contains(point.OrderId))
|
||||
.ToListAsync(cancellationToken);
|
||||
return points
|
||||
.GroupBy(point => point.OrderId)
|
||||
.SelectMany(group => group.OrderByDescending(point => point.SampleTime).Take(perOrderLimit))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<RiskEvent>> LoadRecentRiskEventsAsync(
|
||||
IReadOnlyCollection<Guid> orderIds,
|
||||
int perOrderLimit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (dbContext.Database.IsSqlServer())
|
||||
{
|
||||
return await BuildRecentRiskEventQuery(dbContext, orderIds, perOrderLimit)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var events = await dbContext.RiskEvents
|
||||
.AsNoTracking()
|
||||
.Where(riskEvent => orderIds.Contains(riskEvent.OrderId))
|
||||
.ToListAsync(cancellationToken);
|
||||
return events
|
||||
.GroupBy(riskEvent => riskEvent.OrderId)
|
||||
.SelectMany(group => group.OrderByDescending(riskEvent => riskEvent.OccurredAt).Take(perOrderLimit))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static IQueryable<ProductionTrendPoint> BuildRecentTrendQuery(
|
||||
DashboardDbContext dbContext,
|
||||
IReadOnlyCollection<Guid> orderIds,
|
||||
int perOrderLimit)
|
||||
{
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
return dbContext.ProductionTrendPoints.Where(_ => false);
|
||||
}
|
||||
|
||||
var (placeholders, parameters) = BuildOrderIdParameters(orderIds);
|
||||
parameters.Add(new SqlParameter("@perOrderLimit", Math.Max(1, perOrderLimit)));
|
||||
var sql = $$"""
|
||||
SELECT [ranked].[Id], [ranked].[OrderId], [ranked].[SampleTime], [ranked].[Completion],
|
||||
[ranked].[Risk], [ranked].[PlannedQty], [ranked].[ActualQty], [ranked].[Achievement]
|
||||
FROM (
|
||||
SELECT [point].*, ROW_NUMBER() OVER (
|
||||
PARTITION BY [point].[OrderId]
|
||||
ORDER BY [point].[SampleTime] DESC, [point].[Id] DESC
|
||||
) AS [row_number]
|
||||
FROM [ProductionTrendPoints] AS [point]
|
||||
WHERE [point].[OrderId] IN ({{placeholders}})
|
||||
) AS [ranked]
|
||||
WHERE [ranked].[row_number] <= @perOrderLimit
|
||||
""";
|
||||
|
||||
return dbContext.ProductionTrendPoints.FromSqlRaw(sql, [.. parameters]);
|
||||
}
|
||||
|
||||
private static IQueryable<RiskEvent> BuildRecentRiskEventQuery(
|
||||
DashboardDbContext dbContext,
|
||||
IReadOnlyCollection<Guid> orderIds,
|
||||
int perOrderLimit)
|
||||
{
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
return dbContext.RiskEvents.Where(_ => false);
|
||||
}
|
||||
|
||||
var (placeholders, parameters) = BuildOrderIdParameters(orderIds);
|
||||
parameters.Add(new SqlParameter("@perOrderLimit", Math.Max(1, perOrderLimit)));
|
||||
var sql = $$"""
|
||||
SELECT [ranked].[Id], [ranked].[OrderId], [ranked].[NodeId], [ranked].[OrderCode],
|
||||
[ranked].[NodeName], [ranked].[RiskLevel], [ranked].[Message],
|
||||
[ranked].[HandlingStatus], [ranked].[OccurredAt]
|
||||
FROM (
|
||||
SELECT [event].*, ROW_NUMBER() OVER (
|
||||
PARTITION BY [event].[OrderId]
|
||||
ORDER BY [event].[OccurredAt] DESC, [event].[Id] DESC
|
||||
) AS [row_number]
|
||||
FROM [RiskEvents] AS [event]
|
||||
WHERE [event].[OrderId] IN ({{placeholders}})
|
||||
) AS [ranked]
|
||||
WHERE [ranked].[row_number] <= @perOrderLimit
|
||||
""";
|
||||
|
||||
return dbContext.RiskEvents.FromSqlRaw(sql, [.. parameters]);
|
||||
}
|
||||
|
||||
private static (string Placeholders, List<object> Parameters) BuildOrderIdParameters(
|
||||
IReadOnlyCollection<Guid> orderIds)
|
||||
{
|
||||
var parameters = orderIds
|
||||
.Select((orderId, index) => (object)new SqlParameter($"@orderId{index}", orderId))
|
||||
.ToList();
|
||||
var placeholders = string.Join(", ", Enumerable.Range(0, orderIds.Count).Select(index => $"@orderId{index}"));
|
||||
return (placeholders, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
public sealed class ProgressAggregationService
|
||||
{
|
||||
public void Recalculate(ProductionOrder order, IReadOnlyCollection<ProductionNode> nodes)
|
||||
{
|
||||
var childrenByParent = nodes
|
||||
.Where(node => node.ParentNodeId.HasValue)
|
||||
.GroupBy(node => node.ParentNodeId!.Value)
|
||||
.ToDictionary(group => group.Key, group => group.ToList());
|
||||
|
||||
var roots = nodes.Where(node => node.ParentNodeId is null).ToList();
|
||||
foreach (var rootNode in roots)
|
||||
{
|
||||
RecalculateNode(rootNode, childrenByParent);
|
||||
}
|
||||
|
||||
var root = roots.FirstOrDefault(node => node.NodeType == "order") ?? roots.FirstOrDefault();
|
||||
if (root is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
order.RequiredQty = root.RequiredQty;
|
||||
order.CompletedQty = root.CompletedQty;
|
||||
order.Progress = root.Progress;
|
||||
order.Status = root.Status;
|
||||
order.RiskLevel = root.RiskLevel;
|
||||
order.DelayDays = root.DelayDays;
|
||||
}
|
||||
|
||||
private static void RecalculateNode(
|
||||
ProductionNode node,
|
||||
IReadOnlyDictionary<Guid, List<ProductionNode>> childrenByParent)
|
||||
{
|
||||
if (!childrenByParent.TryGetValue(node.Id, out var children) || children.Count == 0)
|
||||
{
|
||||
node.Progress = Percentage(node.CompletedQty, node.RequiredQty);
|
||||
if (node.Progress >= 100)
|
||||
{
|
||||
node.Status = "done";
|
||||
node.RiskLevel = "normal";
|
||||
node.DelayDays = 0;
|
||||
node.DelayReason = string.Empty;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
RecalculateNode(child, childrenByParent);
|
||||
}
|
||||
|
||||
var childRequiredQty = children.Sum(child => child.RequiredQty);
|
||||
var weightedProgress = childRequiredQty == 0
|
||||
? 0
|
||||
: children.Sum(child => child.Progress * child.RequiredQty) / (double)childRequiredQty;
|
||||
node.RequiredQty = node.RequiredQty > 0 ? node.RequiredQty : childRequiredQty;
|
||||
node.CompletedQty = (int)Math.Round(
|
||||
node.RequiredQty * weightedProgress / 100d,
|
||||
MidpointRounding.AwayFromZero);
|
||||
node.DefectQty = children.Sum(child => child.DefectQty);
|
||||
node.Progress = Math.Clamp(
|
||||
(int)Math.Round(weightedProgress, MidpointRounding.AwayFromZero),
|
||||
0,
|
||||
100);
|
||||
node.DelayDays = children.Max(child => child.DelayDays);
|
||||
node.RiskLevel = AggregateRisk(children);
|
||||
node.Status = AggregateStatus(children);
|
||||
}
|
||||
|
||||
private static int Percentage(int completed, int required)
|
||||
{
|
||||
return required <= 0
|
||||
? 0
|
||||
: Math.Clamp((int)Math.Round(completed * 100d / required, MidpointRounding.AwayFromZero), 0, 100);
|
||||
}
|
||||
|
||||
private static string AggregateRisk(IEnumerable<ProductionNode> children)
|
||||
{
|
||||
var childList = children.ToList();
|
||||
if (childList.Any(child =>
|
||||
child.RiskLevel == "critical"
|
||||
|| child.Status == "blocked"
|
||||
|| child.DelayDays >= 3))
|
||||
{
|
||||
return "critical";
|
||||
}
|
||||
|
||||
return childList.Any(child =>
|
||||
child.RiskLevel == "warning"
|
||||
|| child.Status == "delayed"
|
||||
|| child.DelayDays > 0)
|
||||
? "warning"
|
||||
: "normal";
|
||||
}
|
||||
|
||||
private static string AggregateStatus(IReadOnlyCollection<ProductionNode> children)
|
||||
{
|
||||
if (children.Any(child => child.Status == "blocked"))
|
||||
{
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
if (children.Any(child => child.Status == "delayed" || child.DelayDays > 0))
|
||||
{
|
||||
return "delayed";
|
||||
}
|
||||
|
||||
if (children.All(child => child.Status == "done"))
|
||||
{
|
||||
return "done";
|
||||
}
|
||||
|
||||
return children.Any(child => child.Status == "in_progress" || child.CompletedQty > 0)
|
||||
? "in_progress"
|
||||
: "waiting";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using DongfangHydro.Dashboard.Api.Realtime;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
public sealed class ProgressUpdateService(
|
||||
DashboardDbContext dbContext,
|
||||
ProgressAggregationService aggregationService,
|
||||
IDashboardUpdateQueue updateQueue)
|
||||
{
|
||||
public async Task<UpdateNodeProgressResultDto?> UpdateAsync(
|
||||
Guid nodeId,
|
||||
UpdateNodeProgressRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var node = await dbContext.ProductionNodes
|
||||
.SingleOrDefaultAsync(item => item.Id == nodeId, cancellationToken);
|
||||
if (node is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!request.CompletedQty.HasValue)
|
||||
{
|
||||
throw new ArgumentException("Completed quantity is required.", nameof(request.CompletedQty));
|
||||
}
|
||||
|
||||
var completedQty = request.CompletedQty.Value;
|
||||
if (completedQty < 0 || completedQty > node.RequiredQty)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(request.CompletedQty),
|
||||
$"Completed quantity must be between 0 and {node.RequiredQty}.");
|
||||
}
|
||||
|
||||
var order = await dbContext.ProductionOrders
|
||||
.SingleAsync(item => item.Id == node.OrderId, cancellationToken);
|
||||
var nodes = await dbContext.ProductionNodes
|
||||
.Where(item => item.OrderId == node.OrderId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var recentTrend = await dbContext.ProductionTrendPoints
|
||||
.AsNoTracking()
|
||||
.Where(point => point.OrderId == node.OrderId)
|
||||
.OrderByDescending(point => point.SampleTime)
|
||||
.Take(11)
|
||||
.ToListAsync(cancellationToken);
|
||||
var recentEvents = await dbContext.RiskEvents
|
||||
.AsNoTracking()
|
||||
.Where(riskEvent => riskEvent.OrderId == node.OrderId)
|
||||
.OrderByDescending(riskEvent => riskEvent.OccurredAt)
|
||||
.Take(7)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ExpectedVersion))
|
||||
{
|
||||
throw new ArgumentException("Expected version is required.", nameof(request.ExpectedVersion));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dbContext.Entry(node).Property(item => item.RowVersion).OriginalValue =
|
||||
Convert.FromBase64String(request.ExpectedVersion);
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new ArgumentException("Expected version is not valid Base64.", nameof(request.ExpectedVersion), exception);
|
||||
}
|
||||
|
||||
if (node.NodeType is not ("process" or "material"))
|
||||
{
|
||||
throw new ArgumentException("Only process or material nodes can receive direct progress updates.", nameof(nodeId));
|
||||
}
|
||||
|
||||
var effectiveStatus = NormalizeStatus(request.Status ?? node.Status);
|
||||
if (completedQty > 0 && completedQty < node.RequiredQty && effectiveStatus == "waiting")
|
||||
{
|
||||
effectiveStatus = "in_progress";
|
||||
}
|
||||
var effectiveRisk = NormalizeRisk(request.RiskLevel ?? node.RiskLevel);
|
||||
var effectiveDelayDays = Math.Max(0, request.DelayDays ?? node.DelayDays);
|
||||
var effectiveDelayReason = request.DelayReason ?? node.DelayReason;
|
||||
var effectiveActualStart = request.ActualStart
|
||||
?? node.ActualStart
|
||||
?? (completedQty > 0 ? DateTime.Today : null);
|
||||
var effectiveActualEnd = completedQty >= node.RequiredQty
|
||||
? request.ActualEnd ?? node.ActualEnd ?? DateTime.Today
|
||||
: request.ActualEnd;
|
||||
ValidateState(
|
||||
completedQty,
|
||||
node.RequiredQty,
|
||||
effectiveStatus,
|
||||
effectiveRisk,
|
||||
effectiveDelayDays,
|
||||
effectiveDelayReason,
|
||||
effectiveActualStart,
|
||||
effectiveActualEnd);
|
||||
|
||||
node.CompletedQty = completedQty;
|
||||
node.DefectQty = Math.Clamp(request.DefectQty ?? node.DefectQty, 0, completedQty);
|
||||
node.DelayDays = effectiveDelayDays;
|
||||
node.DelayReason = effectiveDelayReason;
|
||||
node.Status = effectiveStatus;
|
||||
node.RiskLevel = effectiveRisk;
|
||||
node.ActualStart = effectiveActualStart;
|
||||
node.ActualEnd = effectiveActualEnd;
|
||||
node.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
aggregationService.Recalculate(order, nodes);
|
||||
order.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
var plannedQty = Math.Max(
|
||||
order.CompletedQty,
|
||||
(int)Math.Round(order.RequiredQty * Math.Min(100, order.Progress + 8) / 100d));
|
||||
order.PlanAchievement = plannedQty == 0
|
||||
? 0
|
||||
: Math.Min(120, order.CompletedQty * 100d / plannedQty);
|
||||
|
||||
var trendPoint = new ProductionTrendPoint
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrderId = order.Id,
|
||||
SampleTime = DateTimeOffset.UtcNow,
|
||||
Completion = order.Progress,
|
||||
Risk = nodes.Count(item => item.RiskLevel != "normal"),
|
||||
PlannedQty = plannedQty,
|
||||
ActualQty = order.CompletedQty,
|
||||
Achievement = order.PlanAchievement,
|
||||
};
|
||||
dbContext.ProductionTrendPoints.Add(trendPoint);
|
||||
|
||||
RiskEvent? riskEvent = null;
|
||||
if (node.RiskLevel != "normal" || node.DelayDays > 0 || node.Status is "delayed" or "blocked")
|
||||
{
|
||||
riskEvent = 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 = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.RiskEvents.Add(riskEvent);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
var rootDto = DashboardMapper.BuildTree(nodes)
|
||||
?? throw new InvalidOperationException($"Order {order.Id} has no production tree.");
|
||||
var trendDtos = recentTrend
|
||||
.Append(trendPoint)
|
||||
.OrderBy(point => point.SampleTime)
|
||||
.Select(DashboardMapper.MapTrend)
|
||||
.ToList();
|
||||
var eventDtos = (riskEvent is null ? recentEvents : recentEvents.Prepend(riskEvent))
|
||||
.Take(8)
|
||||
.Select(DashboardMapper.MapRiskEvent)
|
||||
.ToList();
|
||||
var orderDto = DashboardMapper.MapOrder(order, rootDto, trendDtos, eventDtos);
|
||||
var nodeDto = FindNode(orderDto.Root, node.Id)
|
||||
?? throw new InvalidOperationException($"Node {node.Id} disappeared after progress update.");
|
||||
var result = new UpdateNodeProgressResultDto(
|
||||
nodeDto,
|
||||
orderDto,
|
||||
riskEvent is null ? null : DashboardMapper.MapRiskEvent(riskEvent),
|
||||
DashboardMapper.MapTrend(trendPoint));
|
||||
await updateQueue.EnqueueAsync(result, CancellationToken.None);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ProductionNodeDto? FindNode(ProductionNodeDto node, Guid nodeId)
|
||||
{
|
||||
if (node.Id == nodeId)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
var found = FindNode(child, nodeId);
|
||||
if (found is not null)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeStatus(string value)
|
||||
{
|
||||
return value is "waiting" or "in_progress" or "done" or "delayed" or "blocked"
|
||||
? value
|
||||
: throw new ArgumentException($"Unsupported production status '{value}'.", nameof(value));
|
||||
}
|
||||
|
||||
private static string NormalizeRisk(string value)
|
||||
{
|
||||
return value is "normal" or "warning" or "critical"
|
||||
? value
|
||||
: throw new ArgumentException($"Unsupported risk level '{value}'.", nameof(value));
|
||||
}
|
||||
|
||||
private static void ValidateState(
|
||||
int completedQty,
|
||||
int requiredQty,
|
||||
string status,
|
||||
string riskLevel,
|
||||
int delayDays,
|
||||
string delayReason,
|
||||
DateTime? actualStart,
|
||||
DateTime? actualEnd)
|
||||
{
|
||||
if (delayReason.Length > 500)
|
||||
{
|
||||
throw new ArgumentException("Delay reason cannot exceed 500 characters.", nameof(delayReason));
|
||||
}
|
||||
|
||||
if (completedQty < requiredQty && status == "done")
|
||||
{
|
||||
throw new ArgumentException("A process cannot be done before its planned quantity is complete.", nameof(status));
|
||||
}
|
||||
|
||||
if (completedQty < requiredQty && actualEnd.HasValue)
|
||||
{
|
||||
throw new ArgumentException("An incomplete process cannot have an actual end date.", nameof(actualEnd));
|
||||
}
|
||||
|
||||
|
||||
if (actualStart.HasValue && actualEnd.HasValue && actualEnd.Value < actualStart.Value)
|
||||
{
|
||||
throw new ArgumentException("Actual end date cannot be earlier than actual start date.", nameof(actualEnd));
|
||||
}
|
||||
|
||||
if (status == "blocked" && riskLevel != "critical")
|
||||
{
|
||||
throw new ArgumentException("A blocked process must have critical risk.", nameof(riskLevel));
|
||||
}
|
||||
|
||||
if ((status == "delayed" || delayDays > 0) && riskLevel == "normal")
|
||||
{
|
||||
throw new ArgumentException("A delayed process cannot have normal risk.", nameof(riskLevel));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Database": {
|
||||
"AutoMigrate": true,
|
||||
"Seed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DashboardDb": ""
|
||||
},
|
||||
"Database": {
|
||||
"AutoMigrate": false,
|
||||
"Seed": false
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173"
|
||||
]
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class BomDataApiTests(DashboardApiFactory factory)
|
||||
: IClassFixture<DashboardApiFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task MaterialQuotas_supports_category_keyword_and_paging()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
Guid orderId;
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
|
||||
orderId = await dbContext.ProductionOrders.Select(order => order.Id).FirstAsync();
|
||||
dbContext.MaterialQuotas.AddRange(
|
||||
CreateQuota(orderId, "钢板", "1010100182", "钢板/Q235B", 9),
|
||||
CreateQuota(orderId, "标准件", "1110100001", "螺栓/8.8级", 188));
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var response = await client.GetAsync(
|
||||
$"/api/orders/{orderId}/material-quotas?category=钢板&keyword=Q235B&page=1&pageSize=10");
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(1, body.GetProperty("total").GetInt32());
|
||||
var item = Assert.Single(body.GetProperty("items").EnumerateArray());
|
||||
Assert.Equal("钢板", item.GetProperty("category").GetString());
|
||||
Assert.Equal("1010100182", item.GetProperty("materialCode").GetString());
|
||||
Assert.Equal(33.61m, item.GetProperty("netWeight").GetDecimal());
|
||||
Assert.Equal(35.6266m, item.GetProperty("consumptionQuota").GetDecimal());
|
||||
Assert.Equal("33.61", item.GetProperty("netWeightText").GetString());
|
||||
Assert.Equal("35.6266", item.GetProperty("consumptionQuotaText").GetString());
|
||||
Assert.Equal(9, item.GetProperty("sourceRow").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tree_exposes_imported_bom_fields()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
Guid orderId;
|
||||
Guid nodeId;
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
|
||||
var node = await dbContext.ProductionNodes.FirstAsync(item => item.NodeType == "process");
|
||||
orderId = node.OrderId;
|
||||
nodeId = node.Id;
|
||||
node.Specification = "M16";
|
||||
node.Material = "标准件";
|
||||
node.UnitWeight = 0.2m;
|
||||
node.TotalWeight = 1.6m;
|
||||
node.UnitWeightText = "0.2";
|
||||
node.TotalWeightText = "1.6";
|
||||
node.BomQuantity = -8m;
|
||||
node.BomQuantityText = "-8";
|
||||
node.Remark = "镀锌";
|
||||
node.SourceSequence = "1.1.2";
|
||||
node.SourceSheet = "明细";
|
||||
node.SourceRow = 10;
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
|
||||
var nodeJson = FindNode(root, nodeId);
|
||||
|
||||
Assert.Equal("M16", nodeJson.GetProperty("specification").GetString());
|
||||
Assert.Equal("标准件", nodeJson.GetProperty("material").GetString());
|
||||
Assert.Equal(0.2m, nodeJson.GetProperty("unitWeight").GetDecimal());
|
||||
Assert.Equal(1.6m, nodeJson.GetProperty("totalWeight").GetDecimal());
|
||||
Assert.Equal("0.2", nodeJson.GetProperty("unitWeightText").GetString());
|
||||
Assert.Equal("1.6", nodeJson.GetProperty("totalWeightText").GetString());
|
||||
Assert.Equal(-8m, nodeJson.GetProperty("bomQuantity").GetDecimal());
|
||||
Assert.Equal("-8", nodeJson.GetProperty("bomQuantityText").GetString());
|
||||
Assert.Equal("镀锌", nodeJson.GetProperty("remark").GetString());
|
||||
Assert.Equal("1.1.2", nodeJson.GetProperty("sourceSequence").GetString());
|
||||
Assert.Equal("明细", nodeJson.GetProperty("sourceSheet").GetString());
|
||||
Assert.Equal(10, nodeJson.GetProperty("sourceRow").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Material_leaf_accepts_direct_progress_updates()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var orderId = Guid.NewGuid();
|
||||
var rootId = Guid.NewGuid();
|
||||
var materialId = Guid.NewGuid();
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
|
||||
dbContext.ProductionOrders.Add(new ProductionOrder
|
||||
{
|
||||
Id = orderId,
|
||||
Code = $"ZZ-MAT-{orderId:N}"[..24],
|
||||
ProductName = "物料报工测试",
|
||||
BatchQty = 1,
|
||||
RequiredQty = 10,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
DataSource = "test",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
RowVersion = [1],
|
||||
});
|
||||
dbContext.ProductionNodes.AddRange(
|
||||
new ProductionNode
|
||||
{
|
||||
Id = rootId,
|
||||
OrderId = orderId,
|
||||
Code = "ROOT",
|
||||
Name = "测试订单",
|
||||
NodeType = "order",
|
||||
SupplyType = "self_made",
|
||||
RequiredQty = 10,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
RowVersion = [1],
|
||||
},
|
||||
new ProductionNode
|
||||
{
|
||||
Id = materialId,
|
||||
OrderId = orderId,
|
||||
ParentNodeId = rootId,
|
||||
Code = "MAT-01",
|
||||
Name = "钢板",
|
||||
Level = 1,
|
||||
NodeType = "material",
|
||||
SupplyType = "purchased",
|
||||
RequiredQty = 10,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
RowVersion = [1],
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var tree = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
|
||||
var material = FindNode(tree, materialId);
|
||||
var response = await client.PatchAsJsonAsync(
|
||||
$"/api/nodes/{materialId}/progress",
|
||||
new
|
||||
{
|
||||
completedQty = 10,
|
||||
expectedVersion = material.GetProperty("version").GetString(),
|
||||
status = "done",
|
||||
riskLevel = "normal",
|
||||
});
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("material", body.GetProperty("node").GetProperty("nodeType").GetString());
|
||||
Assert.Equal(100, body.GetProperty("node").GetProperty("progress").GetInt32());
|
||||
}
|
||||
|
||||
private static MaterialQuota CreateQuota(
|
||||
Guid orderId,
|
||||
string category,
|
||||
string materialCode,
|
||||
string materialName,
|
||||
int sourceRow)
|
||||
{
|
||||
return new MaterialQuota
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrderId = orderId,
|
||||
Category = category,
|
||||
SourceSequence = "1",
|
||||
MaterialCode = materialCode,
|
||||
MaterialName = materialName,
|
||||
Specification = "δ2-1500",
|
||||
Unit = "kg",
|
||||
NetWeight = 33.61m,
|
||||
ConsumptionQuota = 35.6266m,
|
||||
NetWeightText = "33.61",
|
||||
ConsumptionQuotaText = "35.6266",
|
||||
Brand = "无",
|
||||
Remark = string.Empty,
|
||||
SourceSheet = "材料定额",
|
||||
SourceRow = sourceRow,
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement FindNode(JsonElement node, Guid nodeId)
|
||||
{
|
||||
if (node.GetProperty("id").GetGuid() == nodeId)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
foreach (var child in node.GetProperty("children").EnumerateArray())
|
||||
{
|
||||
var match = FindNode(child, nodeId);
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using DongfangHydro.Dashboard.Api.Importing;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class BomImportCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_reads_file_and_switches()
|
||||
{
|
||||
var command = BomImportCommand.Parse(
|
||||
["import-bom", "--file", "C:/data/bom.xlsx", "--purge-demo", "--validate-only", "--force"]);
|
||||
|
||||
Assert.NotNull(command);
|
||||
Assert.Equal("C:/data/bom.xlsx", command.FilePath);
|
||||
Assert.True(command.PurgeDemo);
|
||||
Assert.True(command.ValidateOnly);
|
||||
Assert.True(command.Force);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_returns_null_for_normal_web_host_arguments()
|
||||
{
|
||||
Assert.Null(BomImportCommand.Parse(["--urls", "http://localhost:5080"]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_rejects_import_without_a_file()
|
||||
{
|
||||
var exception = Assert.Throws<BomImportException>(() => BomImportCommand.Parse(["import-bom"]));
|
||||
|
||||
Assert.Contains("--file", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using DongfangHydro.Dashboard.Api.Importing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class BomImportServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Order_code_index_is_not_unique_because_source_document_is_the_import_identity()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<DashboardDbContext>()
|
||||
.UseInMemoryDatabase($"bom-model-{Guid.NewGuid():N}")
|
||||
.Options;
|
||||
using var dbContext = new DashboardDbContext(options);
|
||||
|
||||
var orderType = dbContext.Model.FindEntityType(typeof(ProductionOrder))!;
|
||||
var codeIndex = Assert.Single(orderType.GetIndexes(), index =>
|
||||
index.Properties.Count == 1 && index.Properties[0].Name == nameof(ProductionOrder.Code));
|
||||
|
||||
Assert.False(codeIndex.IsUnique);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_replaces_matching_source_purges_demo_and_is_idempotent()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<DashboardDbContext>()
|
||||
.UseInMemoryDatabase($"bom-import-{Guid.NewGuid():N}")
|
||||
.Options;
|
||||
await using var dbContext = new DashboardDbContext(options);
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
|
||||
dbContext.ProductionOrders.AddRange(
|
||||
CreateOrder("MO-DEMO", "demo", string.Empty),
|
||||
CreateOrder("2437-OLD", "excel", "SG2026-06-033"),
|
||||
CreateOrder("KEEP-001", "manual", "OTHER-DOC"));
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var service = new BomImportService(dbContext);
|
||||
var document = CreateDocument();
|
||||
var first = await service.ImportAsync(document, purgeDemo: true, CancellationToken.None);
|
||||
var progressedNode = await dbContext.ProductionNodes.SingleAsync(node => node.SourceRow == 6);
|
||||
progressedNode.CompletedQty = 1;
|
||||
progressedNode.Progress = 50;
|
||||
progressedNode.Status = "in_progress";
|
||||
await dbContext.SaveChangesAsync();
|
||||
var second = await service.ImportAsync(document, purgeDemo: true, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, first.RemovedDemoOrders);
|
||||
Assert.Equal(1, first.ReplacedOrders);
|
||||
Assert.Equal(0, second.ReplacedOrders);
|
||||
Assert.Equal(2, second.TotalOrders);
|
||||
Assert.Equal(2, await dbContext.ProductionOrders.CountAsync());
|
||||
Assert.DoesNotContain(await dbContext.ProductionOrders.ToListAsync(), order => order.DataSource == "demo");
|
||||
Assert.Contains(await dbContext.ProductionOrders.ToListAsync(), order => order.Code == "KEEP-001");
|
||||
|
||||
var importedOrder = await dbContext.ProductionOrders.SingleAsync(
|
||||
order => order.SourceDocumentCode == "SG2026-06-033");
|
||||
Assert.Equal("2437-QM0201", importedOrder.Code);
|
||||
Assert.Equal("excel", importedOrder.DataSource);
|
||||
Assert.Null(importedOrder.PlannedStart);
|
||||
Assert.Null(importedOrder.PlannedEnd);
|
||||
|
||||
var nodes = await dbContext.ProductionNodes
|
||||
.Where(node => node.OrderId == importedOrder.Id)
|
||||
.OrderBy(node => node.Level)
|
||||
.ThenBy(node => node.SourceRow)
|
||||
.ToListAsync();
|
||||
Assert.Equal(3, nodes.Count);
|
||||
Assert.Equal("order", nodes[0].NodeType);
|
||||
Assert.Equal("part", nodes[1].NodeType);
|
||||
Assert.Equal("material", nodes[2].NodeType);
|
||||
Assert.Equal(nodes[0].Id, nodes[1].ParentNodeId);
|
||||
Assert.Equal(nodes[1].Id, nodes[2].ParentNodeId);
|
||||
Assert.Equal(1, nodes.Single(node => node.SourceRow == 6).CompletedQty);
|
||||
Assert.Equal(50, nodes.Single(node => node.SourceRow == 6).Progress);
|
||||
Assert.Equal("in_progress", nodes.Single(node => node.SourceRow == 6).Status);
|
||||
|
||||
var quota = await dbContext.MaterialQuotas.SingleAsync(item => item.OrderId == importedOrder.Id);
|
||||
Assert.Equal("钢板", quota.Category);
|
||||
Assert.Equal(33.61m, quota.NetWeight);
|
||||
Assert.Equal(9, quota.SourceRow);
|
||||
|
||||
var forced = await service.ImportAsync(
|
||||
document,
|
||||
purgeDemo: true,
|
||||
CancellationToken.None,
|
||||
forceReplace: true);
|
||||
Assert.Equal(1, forced.ReplacedOrders);
|
||||
Assert.False(forced.SkippedUnchanged);
|
||||
Assert.Equal(0, (await dbContext.ProductionNodes.SingleAsync(node => node.SourceRow == 6)).Progress);
|
||||
}
|
||||
|
||||
private static ProductionOrder CreateOrder(string code, string dataSource, string sourceDocumentCode)
|
||||
{
|
||||
return new ProductionOrder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = code,
|
||||
ProductName = code,
|
||||
DataSource = dataSource,
|
||||
SourceDocumentCode = sourceDocumentCode,
|
||||
BatchQty = 1,
|
||||
RequiredQty = 1,
|
||||
Status = "waiting",
|
||||
RiskLevel = "normal",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
private static BomImportDocument CreateDocument()
|
||||
{
|
||||
return new BomImportDocument(
|
||||
new BomImportMetadata(
|
||||
"SG2026-06-033",
|
||||
"2437-QM0201",
|
||||
"青峪口水库工程",
|
||||
"青峪口尾水单向门机 2×160/10",
|
||||
"电站尾水2×160kN/100kN单向门式启闭机",
|
||||
1,
|
||||
"bom.xlsx",
|
||||
new string('a', 64)),
|
||||
[
|
||||
new BomImportNode(2, null, "1", "P-01", "主起升机构", "", 1, "部件", 10, 10, "", 1, "part", "self_made"),
|
||||
new BomImportNode(6, 2, "1.1", "GB/T700", "钢板", "δ10", 2, "Q235B", 5, 10, "", 2, "material", "purchased"),
|
||||
],
|
||||
[
|
||||
new MaterialQuotaImportRow(9, "钢板", "1", "1010100182", "钢板/Q235B", "δ2-1500", "kg", null, 33.61m, 35.6266m, "无", ""),
|
||||
],
|
||||
2,
|
||||
[]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using ClosedXML.Excel;
|
||||
using DongfangHydro.Dashboard.Api.Importing;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class BomWorkbookParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_builds_hierarchy_and_preserves_repeated_material_occurrences()
|
||||
{
|
||||
using var stream = CreateWorkbook();
|
||||
|
||||
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
|
||||
|
||||
Assert.Equal("SG2026-06-033", document.Metadata.SourceDocumentCode);
|
||||
Assert.Equal("2437-QM0201", document.Metadata.WorkOrderCode);
|
||||
Assert.Equal("青峪口水库工程", document.Metadata.ProjectName);
|
||||
Assert.Equal("青峪口尾水单向门机 2×160/10", document.Metadata.ContractProductName);
|
||||
Assert.Equal("电站尾水2×160kN/100kN单向门式启闭机", document.Metadata.DrawingProductName);
|
||||
Assert.Equal(1, document.Metadata.BatchQuantity);
|
||||
Assert.Equal(3, document.BlockCount);
|
||||
Assert.Single(document.Warnings);
|
||||
Assert.Contains("第 11 行", document.Warnings[0]);
|
||||
|
||||
Assert.Equal(5, document.BomNodes.Count);
|
||||
Assert.Equal(2, document.BomNodes.Count(node => node.Level == 1));
|
||||
Assert.Single(document.BomNodes, node => node.Level == 2);
|
||||
Assert.Equal(2, document.BomNodes.Count(node => node.Level == 3));
|
||||
|
||||
var component = Assert.Single(document.BomNodes, node => node.DrawingNumber == "C-01");
|
||||
Assert.Equal(2, component.ParentSourceRow);
|
||||
Assert.Equal("component", component.NodeType);
|
||||
Assert.Equal("self_made", component.SupplyType);
|
||||
Assert.Equal("焊接件", component.Specification);
|
||||
Assert.Equal("装配件", component.Material);
|
||||
Assert.Equal(4m, component.UnitWeight);
|
||||
Assert.Equal(8m, component.TotalWeight);
|
||||
Assert.Equal("备注", component.Remark);
|
||||
|
||||
var bolts = document.BomNodes.Where(node => node.Name == "螺栓").OrderBy(node => node.SourceRow).ToList();
|
||||
Assert.Equal(2, bolts.Count);
|
||||
Assert.All(bolts, node => Assert.Equal(6, node.ParentSourceRow));
|
||||
Assert.Null(bolts[0].Quantity);
|
||||
Assert.Equal("/", bolts[0].QuantityText);
|
||||
Assert.Equal(-8m, bolts[1].Quantity);
|
||||
Assert.Null(bolts[1].UnitWeight);
|
||||
Assert.Null(bolts[1].TotalWeight);
|
||||
Assert.Equal("95L", bolts[1].UnitWeightText);
|
||||
Assert.Equal("170L", bolts[1].TotalWeightText);
|
||||
Assert.All(bolts, node => Assert.Equal("material", node.NodeType));
|
||||
Assert.All(bolts, node => Assert.Equal("purchased", node.SupplyType));
|
||||
Assert.NotEqual(bolts[0].SourceRow, bolts[1].SourceRow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_material_quota_categories_and_nullable_quantity()
|
||||
{
|
||||
using var stream = CreateWorkbook();
|
||||
|
||||
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
|
||||
|
||||
Assert.Equal(2, document.MaterialQuotas.Count);
|
||||
var steel = document.MaterialQuotas[0];
|
||||
Assert.Equal("钢板", steel.Category);
|
||||
Assert.Equal("1010100182", steel.MaterialCode);
|
||||
Assert.Equal("钢板/Q235B", steel.MaterialName);
|
||||
Assert.Equal("δ2-1500", steel.Specification);
|
||||
Assert.Equal("kg", steel.Unit);
|
||||
Assert.Null(steel.Quantity);
|
||||
Assert.Null(steel.NetWeight);
|
||||
Assert.Equal("/", steel.NetWeightText);
|
||||
Assert.Equal(21m, steel.ConsumptionQuota);
|
||||
Assert.Equal("21件", steel.ConsumptionQuotaText);
|
||||
Assert.Equal("无", steel.Brand);
|
||||
|
||||
Assert.Equal("外协外购", document.MaterialQuotas[1].Category);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_rejects_a_block_whose_parent_was_not_declared()
|
||||
{
|
||||
using var stream = CreateWorkbook(orphanParent: true);
|
||||
|
||||
var exception = Assert.Throws<BomImportException>(() =>
|
||||
BomWorkbookParser.Parse(stream, "test-bom.xlsx"));
|
||||
|
||||
Assert.Contains("父节点", exception.Message);
|
||||
Assert.Contains("P-X", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_imports_a_material_quota_row_without_a_material_code()
|
||||
{
|
||||
using var stream = CreateWorkbook(malformedQuota: true);
|
||||
|
||||
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
|
||||
var uncoded = Assert.Single(document.MaterialQuotas, item => item.SourceRow == 13);
|
||||
|
||||
Assert.Equal(string.Empty, uncoded.MaterialCode);
|
||||
Assert.Equal("缺少编码的物料", uncoded.MaterialName);
|
||||
Assert.Equal("件", uncoded.Unit);
|
||||
}
|
||||
|
||||
private static MemoryStream CreateWorkbook(bool orphanParent = false, bool malformedQuota = false)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var detail = workbook.Worksheets.Add("明细");
|
||||
detail.Cell("A1").Value = "序号";
|
||||
detail.Cell("B1").Value = "图号";
|
||||
detail.Cell("C1").Value = "名称";
|
||||
detail.Cell("D1").Value = "型号规格";
|
||||
detail.Cell("E1").Value = "数量";
|
||||
detail.Cell("F1").Value = "材料";
|
||||
detail.Cell("G1").Value = "单重";
|
||||
detail.Cell("H1").Value = "总重";
|
||||
detail.Cell("I1").Value = "备注";
|
||||
|
||||
SetRow(detail, 2, 1, "P-01", "主起升机构", null, 1, "部件", 10, 10, null);
|
||||
SetRow(detail, 3, 2, "P-02", "门架", null, 1, "装焊件", 20, 20, null);
|
||||
SetRow(detail, 5, 1, orphanParent ? "P-X" : "P-01", "主起升机构", null, 1, "部件", 10, 10, null);
|
||||
SetRow(detail, 6, 1.1, "C-01", "机架", "焊接件", 2, "装配件", 4, 8, "备注");
|
||||
SetRow(detail, 8, 1.1, "C-01", "机架", "焊接件", 2, "装配件", 4, 8, "备注");
|
||||
SetRow(detail, 9, 1.11, "GB/T5783", "螺栓", "M12", 4, "标准件", 0.1, 0.4, null);
|
||||
detail.Cell(9, 5).Value = "/";
|
||||
SetRow(detail, 10, 1.12, "GB/T5783", "螺栓", "M16", -8, "标准件", 0.2, 1.6, "厂家提供");
|
||||
detail.Cell(10, 7).Value = "95L";
|
||||
detail.Cell(10, 8).Value = "170L";
|
||||
detail.Cell(11, 1).Value = "1.1.3";
|
||||
|
||||
var quota = workbook.Worksheets.Add("材料定额");
|
||||
quota.Cell("A1").Value = "产品材料定额汇总表";
|
||||
quota.Cell("A2").Value = "编号:SG2026-06-033";
|
||||
quota.Cell("A3").Value = "合同(项目)名称:青峪口水库工程";
|
||||
quota.Cell("H3").Value = "工作令号";
|
||||
quota.Cell("I3").Value = "2437-QM0201";
|
||||
quota.Cell("A4").Value = "产品合同名称:青峪口尾水单向门机 2×160/10";
|
||||
quota.Cell("A5").Value = "产品图纸名称:电站尾水2×160kN/100kN单向门式启闭机";
|
||||
quota.Cell("A6").Value = "说明:本表为1套产品定额";
|
||||
var headers = new[] { "序号", "物料编码", "物料名称", "型号规格", "单位", "数量", "净重", "消耗定额", "品牌", "备注" };
|
||||
for (var column = 1; column <= headers.Length; column++)
|
||||
{
|
||||
quota.Cell(7, column).Value = headers[column - 1];
|
||||
}
|
||||
|
||||
quota.Cell("A8").Value = "一";
|
||||
quota.Cell("B8").Value = "钢板";
|
||||
SetQuotaRow(quota, 9, 1, "1010100182", "钢板/Q235B", "δ2-1500", "kg", null, 33.61, 35.6266, "无", null);
|
||||
quota.Cell(9, 7).Value = "/";
|
||||
quota.Cell(9, 8).Value = "21件";
|
||||
quota.Cell("A11").Value = "二";
|
||||
quota.Cell("B11").Value = "外协外购";
|
||||
SetQuotaRow(quota, 12, 1, "2050000001", "减速器", "ZQ-500", "台", 1, 100, 100, "", "外购");
|
||||
if (malformedQuota)
|
||||
{
|
||||
quota.Cell(13, 1).Value = 2;
|
||||
quota.Cell(13, 3).Value = "缺少编码的物料";
|
||||
quota.Cell(13, 5).Value = "件";
|
||||
}
|
||||
quota.Cell(14, 1).Value = "编码: 编制: 校核: 审核: 日期:2026.6.16";
|
||||
|
||||
var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static void SetRow(
|
||||
IXLWorksheet worksheet,
|
||||
int row,
|
||||
double sequence,
|
||||
string drawingNumber,
|
||||
string name,
|
||||
string? specification,
|
||||
int quantity,
|
||||
string material,
|
||||
double unitWeight,
|
||||
double totalWeight,
|
||||
string? remark)
|
||||
{
|
||||
worksheet.Cell(row, 1).Value = sequence;
|
||||
worksheet.Cell(row, 2).Value = drawingNumber;
|
||||
worksheet.Cell(row, 3).Value = name;
|
||||
worksheet.Cell(row, 4).Value = specification;
|
||||
worksheet.Cell(row, 5).Value = quantity;
|
||||
worksheet.Cell(row, 6).Value = material;
|
||||
worksheet.Cell(row, 7).Value = unitWeight;
|
||||
worksheet.Cell(row, 8).Value = totalWeight;
|
||||
worksheet.Cell(row, 9).Value = remark;
|
||||
}
|
||||
|
||||
private static void SetQuotaRow(
|
||||
IXLWorksheet worksheet,
|
||||
int row,
|
||||
int sequence,
|
||||
string materialCode,
|
||||
string materialName,
|
||||
string specification,
|
||||
string unit,
|
||||
double? quantity,
|
||||
double netWeight,
|
||||
double consumptionQuota,
|
||||
string brand,
|
||||
string? remark)
|
||||
{
|
||||
worksheet.Cell(row, 1).Value = sequence;
|
||||
worksheet.Cell(row, 2).Value = materialCode;
|
||||
worksheet.Cell(row, 3).Value = materialName;
|
||||
worksheet.Cell(row, 4).Value = specification;
|
||||
worksheet.Cell(row, 5).Value = unit;
|
||||
if (quantity.HasValue)
|
||||
{
|
||||
worksheet.Cell(row, 6).Value = quantity.Value;
|
||||
}
|
||||
|
||||
worksheet.Cell(row, 7).Value = netWeight;
|
||||
worksheet.Cell(row, 8).Value = consumptionQuota;
|
||||
worksheet.Cell(row, 9).Value = brand;
|
||||
worksheet.Cell(row, 10).Value = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Realtime;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class DashboardApiTests(DashboardApiFactory factory)
|
||||
: IClassFixture<DashboardApiFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task Orders_SearchesSeededWaterConservancyProductNames()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/orders?keyword=门架&page=1&pageSize=20");
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.True(body.GetProperty("total").GetInt32() >= 1);
|
||||
Assert.Contains(
|
||||
body.GetProperty("items").EnumerateArray(),
|
||||
item => item.GetProperty("productName").GetString()!.Contains("门架", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OrderTree_ReturnsTheCompleteSeededHierarchy()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
|
||||
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
|
||||
|
||||
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
|
||||
|
||||
Assert.Equal("order", root.GetProperty("nodeType").GetString());
|
||||
Assert.Equal(66, CountNodes(root));
|
||||
Assert.Equal(1, CountNodes(root, "order"));
|
||||
Assert.Equal(5, CountNodes(root, "part"));
|
||||
Assert.Equal(15, CountNodes(root, "component"));
|
||||
Assert.Equal(45, CountNodes(root, "process"));
|
||||
Assert.False(string.IsNullOrWhiteSpace(FindFirstProcess(root).GetProperty("version").GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateProgress_RecalculatesTheOrderAndReturnsTheUpdatedNode()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
factory.UpdateQueue.Reset();
|
||||
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
|
||||
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
|
||||
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
|
||||
var process = FindFirstProcess(root);
|
||||
|
||||
var response = await client.PatchAsJsonAsync(
|
||||
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
|
||||
new
|
||||
{
|
||||
completedQty = process.GetProperty("requiredQty").GetInt32(),
|
||||
expectedVersion = process.GetProperty("version").GetString(),
|
||||
status = "done",
|
||||
riskLevel = "normal",
|
||||
delayDays = 0,
|
||||
delayReason = string.Empty,
|
||||
});
|
||||
var updated = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(100, updated.GetProperty("node").GetProperty("progress").GetInt32());
|
||||
Assert.Equal("done", updated.GetProperty("node").GetProperty("status").GetString());
|
||||
Assert.Equal(orderId, updated.GetProperty("order").GetProperty("id").GetString());
|
||||
Assert.Equal(1, factory.UpdateQueue.EnqueueCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateProgress_RejectsARequestWithoutCompletedQuantity()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var process = await GetFirstProcessAsync(client);
|
||||
|
||||
var response = await client.PatchAsJsonAsync(
|
||||
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
|
||||
new { status = "in_progress" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateProgress_RejectsDoneStatusBeforeThePlannedQuantityIsComplete()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var process = await GetFirstProcessAsync(client);
|
||||
var requiredQty = process.GetProperty("requiredQty").GetInt32();
|
||||
|
||||
var response = await client.PatchAsJsonAsync(
|
||||
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
|
||||
new
|
||||
{
|
||||
completedQty = requiredQty - 1,
|
||||
expectedVersion = process.GetProperty("version").GetString(),
|
||||
status = "done",
|
||||
riskLevel = "normal",
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateProgress_RequiresTheClientsExpectedVersion()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
var process = await GetFirstProcessAsync(client);
|
||||
|
||||
var response = await client.PatchAsJsonAsync(
|
||||
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
|
||||
new
|
||||
{
|
||||
completedQty = process.GetProperty("completedQty").GetInt32(),
|
||||
status = process.GetProperty("status").GetString(),
|
||||
riskLevel = process.GetProperty("riskLevel").GetString(),
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private static int CountNodes(JsonElement node)
|
||||
{
|
||||
var count = 1;
|
||||
if (node.TryGetProperty("children", out var children))
|
||||
{
|
||||
count += children.EnumerateArray().Sum(CountNodes);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int CountNodes(JsonElement node, string nodeType)
|
||||
{
|
||||
var count = node.GetProperty("nodeType").GetString() == nodeType ? 1 : 0;
|
||||
if (node.TryGetProperty("children", out var children))
|
||||
{
|
||||
count += children.EnumerateArray().Sum(child => CountNodes(child, nodeType));
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> GetFirstProcessAsync(HttpClient client)
|
||||
{
|
||||
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
|
||||
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
|
||||
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
|
||||
return FindFirstProcess(root);
|
||||
}
|
||||
|
||||
private static JsonElement FindFirstProcess(JsonElement node)
|
||||
{
|
||||
if (node.GetProperty("nodeType").GetString() == "process")
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
foreach (var child in node.GetProperty("children").EnumerateArray())
|
||||
{
|
||||
var match = FindFirstProcess(child);
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DashboardApiFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly string databaseName = $"dashboard-tests-{Guid.NewGuid():N}";
|
||||
|
||||
public RecordingDashboardUpdateQueue UpdateQueue { get; } = new();
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Database:AutoMigrate"] = "true",
|
||||
["Database:Seed"] = "true",
|
||||
});
|
||||
});
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<DbContextOptions<DashboardDbContext>>();
|
||||
services.AddDbContext<DashboardDbContext>(options =>
|
||||
options.UseInMemoryDatabase(databaseName));
|
||||
services.RemoveAll<IDashboardUpdateQueue>();
|
||||
services.AddSingleton<IDashboardUpdateQueue>(UpdateQueue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RecordingDashboardUpdateQueue : IDashboardUpdateQueue
|
||||
{
|
||||
public int EnqueueCount { get; private set; }
|
||||
|
||||
public ValueTask EnqueueAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnqueueCount++;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
EnqueueCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.20" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DongfangHydro.Dashboard.Api\DongfangHydro.Dashboard.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,132 @@
|
||||
using DongfangHydro.Dashboard.Api.Domain;
|
||||
using DongfangHydro.Dashboard.Api.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class ProgressAggregationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Recalculate_WeightsProgressAndBubblesRiskAndDelayToTheOrder()
|
||||
{
|
||||
var order = new ProductionOrder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "MO-TEST-001",
|
||||
ProductName = "青峪口尾水单向门机 2×160/10",
|
||||
};
|
||||
var root = Node(order.Id, "ROOT", null, "order", 30, 0);
|
||||
var completed = Node(order.Id, "P-01", root.Id, "part", 10, 10);
|
||||
var delayed = Node(order.Id, "P-02", root.Id, "part", 20, 10);
|
||||
delayed.Status = "delayed";
|
||||
delayed.RiskLevel = "critical";
|
||||
delayed.DelayDays = 3;
|
||||
|
||||
new ProgressAggregationService().Recalculate(order, [root, completed, delayed]);
|
||||
|
||||
Assert.Equal(67, root.Progress);
|
||||
Assert.Equal(20, root.CompletedQty);
|
||||
Assert.Equal("delayed", root.Status);
|
||||
Assert.Equal("critical", root.RiskLevel);
|
||||
Assert.Equal(3, root.DelayDays);
|
||||
Assert.Equal(67, order.Progress);
|
||||
Assert.Equal(20, order.CompletedQty);
|
||||
Assert.Equal("delayed", order.Status);
|
||||
Assert.Equal("critical", order.RiskLevel);
|
||||
Assert.Equal(3, order.DelayDays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recalculate_MarksParentDoneWhenAllChildrenAreDone()
|
||||
{
|
||||
var order = new ProductionOrder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "MO-TEST-002",
|
||||
ProductName = "门架",
|
||||
};
|
||||
var root = Node(order.Id, "ROOT", null, "order", 10, 0);
|
||||
var childA = Node(order.Id, "P-01", root.Id, "part", 4, 4);
|
||||
var childB = Node(order.Id, "P-02", root.Id, "part", 6, 6);
|
||||
|
||||
new ProgressAggregationService().Recalculate(order, [root, childA, childB]);
|
||||
|
||||
Assert.Equal(100, root.Progress);
|
||||
Assert.Equal("done", root.Status);
|
||||
Assert.Equal("normal", root.RiskLevel);
|
||||
Assert.Equal(100, order.Progress);
|
||||
Assert.Equal("done", order.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recalculate_KeepsTheParentsPlannedQuantityWhileUsingChildrenAsWeights()
|
||||
{
|
||||
var order = new ProductionOrder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "MO-TEST-003",
|
||||
ProductName = "大车行走机构",
|
||||
};
|
||||
var root = Node(order.Id, "ROOT", null, "order", 5, 0);
|
||||
var childA = Node(order.Id, "P-01", root.Id, "part", 10, 10);
|
||||
var childB = Node(order.Id, "P-02", root.Id, "part", 20, 10);
|
||||
|
||||
new ProgressAggregationService().Recalculate(order, [root, childA, childB]);
|
||||
|
||||
Assert.Equal(5, root.RequiredQty);
|
||||
Assert.Equal(3, root.CompletedQty);
|
||||
Assert.Equal(5, order.RequiredQty);
|
||||
Assert.Equal(3, order.CompletedQty);
|
||||
Assert.Equal(67, order.Progress);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("blocked", 0, "critical")]
|
||||
[InlineData("delayed", 1, "warning")]
|
||||
[InlineData("in_progress", 3, "critical")]
|
||||
public void Recalculate_BubblesOperationalRiskEvenWhenTheLeafRiskWasNotSet(
|
||||
string childStatus,
|
||||
int delayDays,
|
||||
string expectedRisk)
|
||||
{
|
||||
var order = new ProductionOrder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "MO-TEST-004",
|
||||
ProductName = "门机电气控制系统",
|
||||
};
|
||||
var root = Node(order.Id, "ROOT", null, "order", 10, 0);
|
||||
var child = Node(order.Id, "OP-10", root.Id, "process", 10, 4);
|
||||
child.Status = childStatus;
|
||||
child.DelayDays = delayDays;
|
||||
child.RiskLevel = "normal";
|
||||
|
||||
new ProgressAggregationService().Recalculate(order, [root, child]);
|
||||
|
||||
Assert.Equal(expectedRisk, root.RiskLevel);
|
||||
Assert.Equal(expectedRisk, order.RiskLevel);
|
||||
}
|
||||
|
||||
private static ProductionNode Node(
|
||||
Guid orderId,
|
||||
string code,
|
||||
Guid? parentId,
|
||||
string nodeType,
|
||||
int requiredQty,
|
||||
int completedQty)
|
||||
{
|
||||
return new ProductionNode
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrderId = orderId,
|
||||
ParentNodeId = parentId,
|
||||
Code = code,
|
||||
Name = code,
|
||||
NodeType = nodeType,
|
||||
RequiredQty = requiredQty,
|
||||
CompletedQty = completedQty,
|
||||
Status = completedQty >= requiredQty ? "done" : "in_progress",
|
||||
RiskLevel = "normal",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using DongfangHydro.Dashboard.Api.Data;
|
||||
using DongfangHydro.Dashboard.Api.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Tests;
|
||||
|
||||
public sealed class SqlServerQueryShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void RecentTrendQuery_UsesAServerSidePerOrderWindow()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<DashboardDbContext>()
|
||||
.UseSqlServer("Server=localhost;Database=query-shape;TrustServerCertificate=True")
|
||||
.Options;
|
||||
using var dbContext = new DashboardDbContext(options);
|
||||
var orderIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
|
||||
var query = DashboardQueryService.BuildRecentTrendQuery(dbContext, orderIds, 12);
|
||||
|
||||
var sql = query.ToQueryString();
|
||||
|
||||
Assert.Contains("ROW_NUMBER", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("PARTITION BY", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
sqlserver:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
container_name: dongfang-hydro-sqlserver
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_PID: "Developer"
|
||||
MSSQL_SA_PASSWORD: "${MSSQL_SA_PASSWORD:?Set MSSQL_SA_PASSWORD before starting SQL Server}"
|
||||
ports:
|
||||
- "127.0.0.1:1433:1433"
|
||||
volumes:
|
||||
- dongfang-hydro-sql-data:/var/opt/mssql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -C -Q 'SELECT 1' || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
dongfang-hydro-sql-data:
|
||||
Reference in New Issue
Block a user