using System.Text.Json;
using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow;
using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow;
namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow;
///
/// ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3b:S8 订单执行链路只读 service skeleton。
/// 仅 GetOrders / GetOrder / GetChain 三个方法;procurement-pivot / aggregate / related-exceptions 延后切片。
/// 任何业务真值(订单总数 / 平均时长 / YY 真值矩阵等)只能来自 DB,本文件零硬编码。
/// 异常计数:从 ado_s8_exception 实时聚合(SourceObjectType=SALES_ORDER + RelatedObjectCode in order_codes + IsDeleted=0)。
///
public class S8OrderFlowService : ITransient
{
private const string ExceptionSourceObjectType = "SALES_ORDER";
private const string ScopeBaselinePpt = "BASELINE_PPT";
private const string ScopeCurrentFiltered = "CURRENT_FILTERED";
private const string BaselineSnapshotCode = "CHAIN_AGGREGATE_BASELINE";
private const string PivotTotalCode = "TOTAL";
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
};
private readonly SqlSugarRepository _orderRep;
private readonly SqlSugarRepository _stageRep;
private readonly SqlSugarRepository _substepRep;
private readonly SqlSugarRepository _unitRep;
private readonly SqlSugarRepository _snapshotRep;
private readonly SqlSugarRepository _pivotRep;
private readonly SqlSugarRepository _exceptionRep;
public S8OrderFlowService(
SqlSugarRepository orderRep,
SqlSugarRepository stageRep,
SqlSugarRepository substepRep,
SqlSugarRepository unitRep,
SqlSugarRepository snapshotRep,
SqlSugarRepository pivotRep,
SqlSugarRepository exceptionRep)
{
_orderRep = orderRep;
_stageRep = stageRep;
_substepRep = substepRep;
_unitRep = unitRep;
_snapshotRep = snapshotRep;
_pivotRep = pivotRep;
_exceptionRep = exceptionRep;
}
/// 订单档案列表(无分页)。当前 baseline 20 单。
public async Task> GetOrdersAsync(AdoS8OrderFlowOrderQueryDto query)
{
var tenantId = query.TenantId ?? 1;
var factoryId = query.FactoryId ?? 1;
var orders = await _orderRep.AsQueryable()
.Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(query.Keyword),
o => o.OrderCode.Contains(query.Keyword!)
|| o.ProductName.Contains(query.Keyword!)
|| o.CustomerName.Contains(query.Keyword!))
.WhereIF(!string.IsNullOrWhiteSpace(query.CurrentFlowCode),
o => o.CurrentOrderFlowCode == query.CurrentFlowCode!)
.WhereIF(!string.IsNullOrWhiteSpace(query.CustomerCode),
o => o.CustomerCode == query.CustomerCode!)
.WhereIF(!string.IsNullOrWhiteSpace(query.ProductLine),
o => o.ProductLine == query.ProductLine!)
.WhereIF(!string.IsNullOrWhiteSpace(query.Region),
o => o.Region == query.Region!)
.WhereIF(!string.IsNullOrWhiteSpace(query.WorkflowStatus),
o => o.WorkflowStatus == query.WorkflowStatus!)
.WhereIF(!string.IsNullOrWhiteSpace(query.ScenarioCode),
o => o.ScenarioCode == query.ScenarioCode!)
.OrderBy(o => o.OrderCode)
.ToListAsync();
if (orders.Count == 0) return new List();
var orderCodes = orders.Select(o => o.OrderCode).Distinct().ToList();
var orderIds = orders.Select(o => o.Id).ToList();
var currentStages = await _stageRep.AsQueryable()
.Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
.Where(s => orderIds.Contains(s.OrderId))
.ToListAsync();
var currentStageMap = currentStages
.GroupBy(s => s.OrderId)
.ToDictionary(g => g.Key, g => g.ToList());
var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, orderCodes);
return orders.Select(o => MapOrderListItem(o, currentStageMap.GetValueOrDefault(o.Id), exceptionCounts)).ToList();
}
/// 单订单详情(不含 substep/unit)。
public async Task GetOrderAsync(string orderCode, long tenantId = 1, long factoryId = 1)
{
if (string.IsNullOrWhiteSpace(orderCode)) return null;
var order = await _orderRep.AsQueryable()
.Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
&& !o.IsDeleted && o.OrderCode == orderCode)
.FirstAsync();
if (order == null) return null;
var stages = await _stageRep.AsQueryable()
.Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
&& !s.IsDeleted && s.OrderId == order.Id)
.OrderBy(s => s.SortNo)
.ToListAsync();
var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List { orderCode });
var listItem = MapOrderListItem(order, stages, exceptionCounts);
return BuildDetail(listItem, stages, substepsByFlow: null, unitsBySubstepId: null);
}
/// 订单链路视图:详情 + lifecycle + L2 substeps + L3 units。procurementPivot 留到 t3c。
public async Task GetChainAsync(string orderCode, long tenantId = 1, long factoryId = 1)
{
if (string.IsNullOrWhiteSpace(orderCode)) return null;
var order = await _orderRep.AsQueryable()
.Where(o => o.TenantId == tenantId && o.FactoryId == factoryId
&& !o.IsDeleted && o.OrderCode == orderCode)
.FirstAsync();
if (order == null) return null;
var stages = await _stageRep.AsQueryable()
.Where(s => s.TenantId == tenantId && s.FactoryId == factoryId
&& !s.IsDeleted && s.OrderId == order.Id)
.OrderBy(s => s.SortNo)
.ToListAsync();
var substeps = await _substepRep.AsQueryable()
.Where(x => x.TenantId == tenantId && x.FactoryId == factoryId
&& !x.IsDeleted && x.OrderId == order.Id)
.OrderBy(x => x.SortNo)
.ToListAsync();
var substepsByFlow = substeps
.GroupBy(x => x.OrderFlowCode)
.ToDictionary(g => g.Key, g => g.ToList());
var substepIds = substeps.Select(x => x.Id).ToList();
var units = substepIds.Count == 0
? new List()
: await _unitRep.AsQueryable()
.Where(u => u.TenantId == tenantId && u.FactoryId == factoryId
&& !u.IsDeleted && substepIds.Contains(u.SubstepId))
.OrderBy(u => u.SortNo)
.ToListAsync();
var unitsBySubstepId = units
.GroupBy(u => u.SubstepId)
.ToDictionary(g => g.Key, g => g.ToList());
var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List { orderCode });
var listItem = MapOrderListItem(order, stages, exceptionCounts);
var detail = BuildDetail(listItem, stages, substepsByFlow, unitsBySubstepId);
return new AdoS8OrderFlowChainDto
{
Order = detail,
ProcurementPivot = null, // t3c 切片接入
};
}
// ─────────────── private mappers ───────────────
/// 从 ado_s8_exception 实时聚合每个 order_code 的异常计数 + 状态。
private async Task> MapExceptionCountsAsync(
long tenantId, long factoryId, List orderCodes)
{
var result = orderCodes.ToDictionary(c => c, _ => (Count: 0, Status: string.Empty));
if (orderCodes.Count == 0) return result;
var rows = await _exceptionRep.AsQueryable()
.Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
.Where(e => e.SourceObjectType == ExceptionSourceObjectType)
.Where(e => orderCodes.Contains(e.RelatedObjectCode!))
.Select(e => new { e.RelatedObjectCode, e.Severity })
.ToListAsync();
foreach (var grp in rows.Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
.GroupBy(r => r.RelatedObjectCode!))
{
var items = grp.ToList();
var status = items.Any(x => string.Equals(x.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase))
? "SERIOUS"
: (items.Count > 0 ? "FOLLOW" : string.Empty);
result[grp.Key] = (items.Count, status);
}
return result;
}
private static AdoS8OrderFlowOrderListItemDto MapOrderListItem(
AdoS8OrderFlowOrder o,
List? lifecycle,
Dictionary exceptionCounts)
{
var current = lifecycle?.FirstOrDefault(s => s.OrderFlowCode == o.CurrentOrderFlowCode);
var counts = exceptionCounts.TryGetValue(o.OrderCode, out var c) ? c : (Count: 0, Status: string.Empty);
return new AdoS8OrderFlowOrderListItemDto
{
OrderCode = o.OrderCode,
ProductName = o.ProductName,
ProductLine = o.ProductLine,
CustomerCode = o.CustomerCode,
CustomerName = o.CustomerName,
CustomerType = o.CustomerType,
Region = o.Region,
Priority = o.Priority,
WorkflowStatus = o.WorkflowStatus,
CurrentOrderFlowCode = o.CurrentOrderFlowCode,
CurrentOrderFlowName = OrderFlowConstants.ResolveName(o.CurrentOrderFlowCode),
CurrentStatus = current?.Status ?? string.Empty,
ReleaseAt = o.ReleaseAt,
TargetCycleDays = o.TargetCycleDays,
ActualCycleDays = o.ActualCycleDays,
CurrentCycleDays = current?.ActualDays,
NodeVarianceDays = current?.NodeVarianceDays,
CumulativeVarianceDays = current?.CumulativeVarianceDays,
ExceptionCount = counts.Count,
ExceptionStatus = counts.Status,
ResponseMinutes = o.ResponseMinutes,
ProcessingMinutes = o.ProcessingMinutes,
TotalLossMinutes = o.TotalLossMinutes,
ScenarioCode = o.ScenarioCode,
DataSource = o.DataSource,
};
}
private static AdoS8OrderFlowOrderDetailDto BuildDetail(
AdoS8OrderFlowOrderListItemDto listItem,
List stages,
Dictionary>? substepsByFlow,
Dictionary>? unitsBySubstepId)
{
var detail = new AdoS8OrderFlowOrderDetailDto
{
OrderCode = listItem.OrderCode,
ProductName = listItem.ProductName,
ProductLine = listItem.ProductLine,
CustomerCode = listItem.CustomerCode,
CustomerName = listItem.CustomerName,
CustomerType = listItem.CustomerType,
Region = listItem.Region,
Priority = listItem.Priority,
WorkflowStatus = listItem.WorkflowStatus,
CurrentOrderFlowCode = listItem.CurrentOrderFlowCode,
CurrentOrderFlowName = listItem.CurrentOrderFlowName,
CurrentStatus = listItem.CurrentStatus,
ReleaseAt = listItem.ReleaseAt,
TargetCycleDays = listItem.TargetCycleDays,
ActualCycleDays = listItem.ActualCycleDays,
CurrentCycleDays = listItem.CurrentCycleDays,
NodeVarianceDays = listItem.NodeVarianceDays,
CumulativeVarianceDays = listItem.CumulativeVarianceDays,
ExceptionCount = listItem.ExceptionCount,
ExceptionStatus = listItem.ExceptionStatus,
ResponseMinutes = listItem.ResponseMinutes,
ProcessingMinutes = listItem.ProcessingMinutes,
TotalLossMinutes = listItem.TotalLossMinutes,
ScenarioCode = listItem.ScenarioCode,
DataSource = listItem.DataSource,
Lifecycle = stages.Select(s => MapStage(s, substepsByFlow, unitsBySubstepId)).ToList(),
};
return detail;
}
private static AdoS8OrderFlowStageDto MapStage(
AdoS8OrderFlowStage s,
Dictionary>? substepsByFlow,
Dictionary>? unitsBySubstepId)
{
var substeps = substepsByFlow != null && substepsByFlow.TryGetValue(s.OrderFlowCode, out var list)
? list.Select(x => MapSubstep(x, unitsBySubstepId)).ToList()
: new List();
return new AdoS8OrderFlowStageDto
{
OrderFlowCode = s.OrderFlowCode,
OrderFlowName = string.IsNullOrEmpty(s.OrderFlowName)
? OrderFlowConstants.ResolveName(s.OrderFlowCode)
: s.OrderFlowName,
OwnerDept = s.OwnerDept,
SortNo = s.SortNo,
PlannedDays = s.PlannedDays,
ActualDays = s.ActualDays,
TargetAt = s.TargetAt,
ActualStartAt = s.ActualStartAt,
ActualEndAt = s.ActualEndAt,
Status = s.Status,
NodeVarianceDays = s.NodeVarianceDays,
CumulativeVarianceDays = s.CumulativeVarianceDays,
Substeps = substeps,
};
}
private static AdoS8OrderFlowSubstepDto MapSubstep(
AdoS8OrderFlowSubstep x,
Dictionary>? unitsBySubstepId)
{
var units = unitsBySubstepId != null && unitsBySubstepId.TryGetValue(x.Id, out var list)
? list.Select(MapUnit).ToList()
: new List();
return new AdoS8OrderFlowSubstepDto
{
SubstepCode = x.SubstepCode,
SubstepName = x.SubstepName,
PiHours = x.PiHours,
ActualHours = x.ActualHours,
Status = x.Status,
SortNo = x.SortNo,
Units = units,
};
}
private static AdoS8OrderFlowSubstepUnitDto MapUnit(AdoS8OrderFlowSubstepUnit u)
=> new()
{
UnitCode = u.UnitCode,
UnitName = u.UnitName,
PiHours = u.PiHours,
ActualHours = u.ActualHours,
Status = u.Status,
SortNo = u.SortNo,
};
// ──────────────────────────────────────────────────────────────────────
// t3c:Aggregate + ProcurementPivot
// ──────────────────────────────────────────────────────────────────────
///
/// 链路聚合视图:
/// scope=BASELINE_PPT → 只读 ado_s8_order_flow_snapshot.stage_snapshots_json
/// scope=CURRENT_FILTERED → 从 order + stage 实时聚合
/// snapshot 缺失时返回空骨架(不做 fallback 硬编码)。
///
public async Task GetAggregateAsync(AdoS8OrderFlowAggregateQueryDto query)
{
var tenantId = query.TenantId ?? 1;
var factoryId = query.FactoryId ?? 1;
var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
return await BuildBaselineAggregateAsync(tenantId, factoryId, scope);
if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, query.OrderCodes);
return EmptyAggregate(scope);
}
private async Task BuildBaselineAggregateAsync(long tenantId, long factoryId, string scope)
{
var snapshot = await _snapshotRep.AsQueryable()
.Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
.Where(s => s.ScopeCode == ScopeBaselinePpt && s.SnapshotCode == BaselineSnapshotCode)
.FirstAsync();
if (snapshot == null) return EmptyAggregate(scope);
var stages = ParseStageSnapshots(snapshot.StageSnapshotsJson);
return new AdoS8OrderFlowAggregateDto
{
Scope = scope,
TotalOrders = snapshot.TotalOrders,
TotalCustomers = snapshot.TotalCustomers,
AvgResponseMinutes = snapshot.AvgResponseMinutes,
AvgProcessingMinutes = snapshot.AvgProcessingMinutes,
AvgLossMinutes = snapshot.AvgLossMinutes,
StageAggregates = ReorderByCanonical(stages),
};
}
private async Task BuildCurrentAggregateAsync(
long tenantId, long factoryId, string scope, List? orderCodes)
{
var orders = await _orderRep.AsQueryable()
.Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted)
.WhereIF(orderCodes != null && orderCodes.Count > 0, o => orderCodes!.Contains(o.OrderCode))
.ToListAsync();
if (orders.Count == 0) return EmptyAggregate(scope);
var orderIds = orders.Select(o => o.Id).ToList();
var stages = await _stageRep.AsQueryable()
.Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted)
.Where(s => orderIds.Contains(s.OrderId))
.ToListAsync();
var responseList = orders.Where(o => o.ResponseMinutes.HasValue).Select(o => (decimal)o.ResponseMinutes!.Value).ToList();
var processingList = orders.Where(o => o.ProcessingMinutes.HasValue).Select(o => (decimal)o.ProcessingMinutes!.Value).ToList();
var lossList = orders.Where(o => o.TotalLossMinutes.HasValue).Select(o => (decimal)o.TotalLossMinutes!.Value).ToList();
var stagesByFlow = stages.GroupBy(s => s.OrderFlowCode).ToDictionary(g => g.Key, g => g.ToList());
var stageAggregates = new List();
foreach (var code in OrderFlowConstants.All)
{
stagesByFlow.TryGetValue(code, out var rows);
stageAggregates.Add(BuildStageAggregateFromRows(code, rows ?? new List()));
}
return new AdoS8OrderFlowAggregateDto
{
Scope = scope,
TotalOrders = orders.Count,
TotalCustomers = orders.Select(o => o.CustomerCode).Where(c => !string.IsNullOrEmpty(c)).Distinct().Count(),
AvgResponseMinutes = AvgOrZero(responseList),
AvgProcessingMinutes = AvgOrZero(processingList),
AvgLossMinutes = AvgOrZero(lossList),
StageAggregates = stageAggregates,
};
}
private static AdoS8OrderFlowStageAggregateDto BuildStageAggregateFromRows(string flowCode, List rows)
{
var dto = new AdoS8OrderFlowStageAggregateDto
{
OrderFlowCode = flowCode,
OrderFlowName = OrderFlowConstants.ResolveName(flowCode),
};
if (rows.Count == 0) return dto;
var plannedList = rows.Select(r => r.PlannedDays).ToList();
var actualList = rows.Where(r => r.ActualDays.HasValue && !string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase))
.Select(r => r.ActualDays!.Value).ToList();
var green = rows.Count(r => string.Equals(r.Status, "green", StringComparison.OrdinalIgnoreCase));
var yellow = rows.Count(r => string.Equals(r.Status, "yellow", StringComparison.OrdinalIgnoreCase));
var red = rows.Count(r => string.Equals(r.Status, "red", StringComparison.OrdinalIgnoreCase));
var pending = rows.Count(r => string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase));
var total = rows.Count;
dto.KpiAvgDays = AvgOrZero(plannedList);
dto.ActualAvgDays = AvgOrZero(actualList);
dto.Green = green;
dto.Yellow = yellow;
dto.Red = red;
dto.Pending = pending;
// 整数百分比;total 不含 0 因为上面已 short-circuit。 percent 因子 100 是公式系数,非业务真值。
dto.OnTimeRate = total > 0 ? (int)Math.Round(green * 100.0 / total) : 0;
return dto;
}
private static List ParseStageSnapshots(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return new List();
try
{
var parsed = JsonSerializer.Deserialize>(json, JsonOpts);
return parsed ?? new List();
}
catch (JsonException)
{
return new List();
}
}
/// 按 OrderFlowConstants.All 重排;缺失 code 补空骨架;多余 code 追加在尾部以避免静默丢数据。
private static List ReorderByCanonical(List input)
{
var byCode = input.Where(x => !string.IsNullOrEmpty(x.OrderFlowCode))
.GroupBy(x => x.OrderFlowCode)
.ToDictionary(g => g.Key, g => g.First());
var ordered = new List();
foreach (var code in OrderFlowConstants.All)
{
if (byCode.TryGetValue(code, out var hit))
{
if (string.IsNullOrEmpty(hit.OrderFlowName))
hit.OrderFlowName = OrderFlowConstants.ResolveName(code);
ordered.Add(hit);
byCode.Remove(code);
}
else
{
ordered.Add(new AdoS8OrderFlowStageAggregateDto
{
OrderFlowCode = code,
OrderFlowName = OrderFlowConstants.ResolveName(code),
});
}
}
ordered.AddRange(byCode.Values);
return ordered;
}
private static AdoS8OrderFlowAggregateDto EmptyAggregate(string scope)
{
var stages = OrderFlowConstants.All
.Select(code => new AdoS8OrderFlowStageAggregateDto
{
OrderFlowCode = code,
OrderFlowName = OrderFlowConstants.ResolveName(code),
})
.ToList();
return new AdoS8OrderFlowAggregateDto { Scope = scope, StageAggregates = stages };
}
private static decimal AvgOrZero(List values)
=> values.Count == 0 ? 0m : values.Average();
///
/// 采购透视视图:
/// scope=BASELINE_PPT → 读 ado_s8_order_flow_procurement_pivot baseline 行(order_id=NULL)
/// status 严格只读 DB;cycle_days 保持 decimal(6,3) 不截断。
/// 本切片正式只支持 BASELINE_PPT;其他 scope 返回空骨架并标注 scope。
///
public async Task GetProcurementPivotAsync(AdoS8OrderFlowProcurementPivotQueryDto query)
{
var tenantId = query.TenantId ?? 1;
var factoryId = query.FactoryId ?? 1;
var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope;
if (!string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase))
return new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
var rows = await _pivotRep.AsQueryable()
.Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted)
.Where(p => p.OrderId == null) // baseline 行
.ToListAsync();
return BuildPivot(scope, rows);
}
private static AdoS8OrderFlowProcurementPivotDto BuildPivot(string scope, List rows)
{
var result = new AdoS8OrderFlowProcurementPivotDto { Scope = scope };
if (rows.Count == 0) return result;
var byMaterial = rows.GroupBy(r => r.MaterialCode)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var (material, list) in byMaterial.OrderBy(kv => kv.Key, StringComparer.Ordinal))
{
// KeyMaterials:取 (supplier=TOTAL, spec=TOTAL) 单元作为该 material 的概览
var grand = list.FirstOrDefault(r => r.SupplierCode == PivotTotalCode && r.SpecCode == PivotTotalCode);
if (grand != null)
{
result.KeyMaterials.Add(new AdoS8OrderFlowKeyMaterialDto
{
MaterialCode = material,
AvgCycleDays = grand.CycleDays,
CycleStatus = grand.Status,
ResultStatus = grand.Status,
});
}
// SupplierBreakdown:spec=TOTAL 的所有 supplier 行(含 supplier=TOTAL)
foreach (var row in list.Where(r => r.SpecCode == PivotTotalCode)
.OrderBy(r => r.SupplierCode == PivotTotalCode ? 1 : 0)
.ThenBy(r => r.SupplierCode, StringComparer.Ordinal))
{
result.SupplierBreakdown.Add(new AdoS8OrderFlowSupplierBreakdownDto
{
MaterialCode = material,
SupplierCode = row.SupplierCode,
AvgCycleDays = row.CycleDays,
Status = row.Status,
});
}
// SpecBreakdown:supplier=TOTAL 的所有 spec 行(含 spec=TOTAL)
foreach (var row in list.Where(r => r.SupplierCode == PivotTotalCode)
.OrderBy(r => r.SpecCode == PivotTotalCode ? 1 : 0)
.ThenBy(r => r.SpecCode, StringComparer.Ordinal))
{
result.SpecBreakdown.Add(new AdoS8OrderFlowSpecBreakdownDto
{
MaterialCode = material,
SpecCode = row.SpecCode,
AvgCycleDays = row.CycleDays,
Status = row.Status,
});
}
// MatrixByMaterial:每个 supplier 一行,cells keyed by spec_code(保留全部 cycle_days 精度)
var matrix = list.GroupBy(r => r.SupplierCode)
.OrderBy(g => g.Key == PivotTotalCode ? 1 : 0)
.ThenBy(g => g.Key, StringComparer.Ordinal)
.Select(g => new AdoS8OrderFlowProcurementMatrixRowDto
{
SupplierCode = g.Key,
Cells = g.ToDictionary(
r => r.SpecCode,
r => new AdoS8OrderFlowProcurementMatrixCellDto
{
CycleDays = r.CycleDays,
Status = r.Status,
}),
})
.ToList();
result.MatrixByMaterial[material] = matrix;
}
return result;
}
}