Files

359 lines
13 KiB
C#

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