| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- using Admin.NET.Plugin.AiDOP.Dto.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- public class S8MonitoringService : ITransient
- {
- private readonly SqlSugarRepository<AdoS8Exception> _rep;
- private readonly SqlSugarRepository<AdoS8ExceptionType> _typeRep;
- // S8-OVERVIEW-DEPT-LABEL-HYDRATION-1:部门名称水合改用 AdoS0DepartmentMaster(DepartmentMaster 表,
- // RecID=1/2 映射真实部门),与 S8DashboardService / S8ExceptionService 主数据水合口径一致。
- // SysOrg 的 Id 为雪花,不与 ado_s8_exception.responsible_dept_id (1/2) 匹配,不能作部门名来源。
- private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
- public S8MonitoringService(
- SqlSugarRepository<AdoS8Exception> rep,
- SqlSugarRepository<AdoS8ExceptionType> typeRep,
- SqlSugarRepository<AdoS0DepartmentMaster> deptRep)
- {
- _rep = rep;
- _typeRep = typeRep;
- _deptRep = deptRep;
- }
- /// <summary>
- /// 9宫格数据:S1-S7 订单健康分布 + S8业务类别汇总 + S9部门汇总。
- /// 数据来源 ado_s8_exception 聚合;异常表为空时返回全 0(不再返回 Demo 数据)。
- /// </summary>
- public async Task<AdoS8OrderGridDto> GetOrderGridAsync(long tenantId = 1, long factoryId = 1)
- {
- // S8-DASHBOARD-DATA-ALIGN-S1S7-1:统一统计基准 module_code IN S1-S7;
- // module_code IS NULL 的 legacy/demo 行不参与大屏聚合(不依赖 scene_code、不映射 legacy scene)。
- var events = await _rep.AsQueryable()
- .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Where(e => S8ModuleCode.All.Contains(e.ModuleCode))
- .Select(e => new
- {
- e.ModuleCode,
- e.Severity,
- e.Status,
- e.TimeoutFlag,
- e.ExceptionTypeCode,
- e.SceneCode,
- e.ResponsibleDeptId,
- e.CreatedAt,
- e.ClosedAt
- })
- .ToListAsync();
- // ── Modules:按 module_code 聚合 ──
- var modules = S8ModuleCode.All.Select(mc =>
- {
- var rows = events.Where(e => e.ModuleCode == mc).ToList();
- var unclosed = rows.Where(e => e.Status != "CLOSED").ToList();
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:red=SERIOUS / yellow=FOLLOW;
- // green 不再来自异常 severity(异常表无「正常」桶),保留 0 兜底,绿色由 useS8StageConfig 兜底。
- var red = unclosed.Count(e => S8SeverityCode.IsSerious(e.Severity));
- var yellow = unclosed.Count(e => S8SeverityCode.IsFollow(e.Severity));
- var green = 0;
- var closed = rows.Count(e => e.Status == "CLOSED");
- var total = rows.Count;
- return new AdoS8ModuleOrderSummary
- {
- ModuleCode = mc,
- ModuleLabel = S8ModuleCode.Label(mc),
- Green = green,
- Yellow = yellow,
- Red = red,
- Total = total,
- Frequency = total,
- AvgProcessHours = AvgHours(rows.Select(r => (r.CreatedAt, r.ClosedAt))),
- CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
- };
- }).ToList();
- // ── ByCategory:按异常类型的 type_code 聚合为 5 大业务类别(CategoryOf) ──
- var typeMap = (await _typeRep.AsQueryable()
- .Where(t => (t.TenantId == 0 && t.FactoryId == 0)
- || (t.TenantId == tenantId && t.FactoryId == factoryId))
- .ToListAsync())
- .GroupBy(t => t.TypeCode)
- .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.FactoryId).First());
- var byCategory = events
- .Where(e => !string.IsNullOrEmpty(e.ExceptionTypeCode) && typeMap.ContainsKey(e.ExceptionTypeCode!))
- .GroupBy(e => CategoryOf(typeMap[e.ExceptionTypeCode!]))
- .Where(g => !string.IsNullOrEmpty(g.Key))
- .Select(g =>
- {
- var total = g.Count();
- var closed = g.Count(e => e.Status == "CLOSED");
- return new AdoS8CategorySummary
- {
- Category = g.Key!,
- Total = total,
- AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))),
- CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
- };
- })
- .OrderBy(c => CategoryOrder(c.Category))
- .ToList();
- // ── ByDept:按 ResponsibleDeptId 聚合,JOIN AdoS0DepartmentMaster(DepartmentMaster)取部门名称 ──
- // S8-OVERVIEW-DEPT-LABEL-HYDRATION-1:原使用 SysOrg 但其 Id 是雪花,与 dept_id=1/2 不匹配 → 100% fallback;
- // 改用 DepartmentMaster(RecID=1=质量部 / RecID=2=生产部),与 dashboard 部门口径对齐。
- var deptIds = events.Select(e => e.ResponsibleDeptId).Where(id => id > 0).Distinct().ToList();
- // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-3):部门水合加 factory_ref_id 约束。
- var deptNameMap = deptIds.Count == 0
- ? new Dictionary<long, string>()
- : (await _deptRep.AsQueryable()
- .Where(d => deptIds.Contains(d.Id) && d.FactoryRefId == factoryId)
- .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
- .ToListAsync())
- .ToDictionary(d => d.Id, d => d.Name ?? string.Empty);
- var byDept = events
- .Where(e => e.ResponsibleDeptId > 0)
- .GroupBy(e => e.ResponsibleDeptId)
- .Select(g =>
- {
- var total = g.Count();
- var closed = g.Count(e => e.Status == "CLOSED");
- return new AdoS8DeptSummary
- {
- DeptName = deptNameMap.TryGetValue(g.Key, out var n) && !string.IsNullOrEmpty(n) ? n : $"部门{g.Key}",
- Total = total,
- AvgProcessHours = AvgHours(g.Select(e => (e.CreatedAt, e.ClosedAt))),
- CloseRate = total == 0 ? 0 : Math.Round(closed * 100.0 / total, 1),
- };
- })
- .OrderByDescending(d => d.Total)
- .ToList();
- return new AdoS8OrderGridDto
- {
- Modules = modules,
- ByCategory = byCategory,
- ByDept = byDept,
- };
- }
- private static double AvgHours(IEnumerable<(DateTime createdAt, DateTime? closedAt)> items)
- {
- var closed = items.Where(x => x.closedAt.HasValue).ToList();
- if (closed.Count == 0) return 0;
- var avg = closed.Average(x => (x.closedAt!.Value - x.createdAt).TotalHours);
- return Math.Round(avg, 1);
- }
- /// <summary>异常类型 → 业务类别(与 Overview 页 5 张类别卡对应)。
- /// 优先级:配置 (AdoS8ExceptionType.MonitoringCategoryKey → KeyToLabel) → legacy hardcode fallback → string.Empty。
- /// 配置缺失或非法 key 时由 CategoryOfLegacy 兜底,保证演示链路不丢类别卡。</summary>
- private static string CategoryOf(AdoS8ExceptionType? t)
- {
- if (t is null) return string.Empty;
- var configured = S8MonitoringCategory.KeyToLabel(t.MonitoringCategoryKey);
- if (!string.IsNullOrEmpty(configured)) return configured;
- return CategoryOfLegacy(t.TypeCode);
- }
- /// <summary>Legacy hardcode 映射,保留现有 15 条 enabled + 7 条 deprecated 兼容分支。
- /// 不在此处补新映射;新 type_code 通过 monitoring_category_key 配置驱动。</summary>
- private static string CategoryOfLegacy(string? typeCode) => typeCode switch
- {
- // 订单评审
- "ORDER_CHANGE" => "订单评审",
- // 总装发货
- "DELIVERY_DELAY" => "总装发货",
- "PENDING_SHIPMENT" => "总装发货",
- // 本体生产(新基线)
- "EQUIP_FAULT" => "本体生产",
- "MFG_MATERIAL_ABNORMAL" => "本体生产",
- "MFG_QUALITY_ABNORMAL" => "本体生产",
- "PRODUCTION_MATERIAL_ABNORMAL" => "本体生产",
- "PRODUCTION_QUALITY_ABNORMAL" => "本体生产",
- "WORK_ORDER_KITTING_ABNORMAL" => "本体生产",
- "WORK_ORDER_ISSUE_ABNORMAL" => "本体生产",
- // 材料采购(新基线)
- "SUPPLIER_ETA_ISSUE" => "材料采购",
- "SUPPLIER_SHIP_ISSUE" => "材料采购",
- "IQC_ISSUE" => "材料采购",
- "WH_PUTAWAY_ISSUE" => "材料采购",
- "WAREHOUSE_RECEIPT_ABNORMAL" => "材料采购",
- // 旧 7 条 deprecated 仍保留兼容(DB 已迁移;此分支防 Overview 临时回放历史 active)
- "MATERIAL_SHORTAGE" => "本体生产",
- "QUALITY_DEFECT" => "本体生产",
- "DIMENSION_DEVIATION" => "本体生产",
- "YIELD_DEFICIT" => "本体生产",
- "WH_KIT_ISSUE" => "本体生产",
- "WH_ISSUE_OUT_ISSUE" => "本体生产",
- "WH_INBOUND_ISSUE" => "材料采购",
- _ => string.Empty,
- };
- private static int CategoryOrder(string category) => category switch
- {
- "订单评审" => 1,
- "产品设计" => 2,
- "材料采购" => 3,
- "本体生产" => 4,
- "总装发货" => 5,
- _ => 99,
- };
- /// <summary>
- /// 异常监控汇总:按 module_code 分组统计红/黄/绿/超时数。
- /// 供综合全景页顶部徽标和模块汇总表使用。
- /// </summary>
- public async Task<AdoS8MonitoringSummaryDto> GetSummaryAsync(AdoS8MonitoringSummaryQueryDto q)
- {
- var query = _rep.AsQueryable()
- .Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
- // S8-DASHBOARD-DATA-ALIGN-S1S7-1:统一统计基准 module_code IN S1-S7。
- .Where(e => S8ModuleCode.All.Contains(e.ModuleCode))
- .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), e => e.SceneCode == q.SceneCode)
- .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), e => e.ModuleCode == q.ModuleCode)
- .WhereIF(q.BizDateFrom.HasValue, e => e.CreatedAt >= q.BizDateFrom!.Value)
- .WhereIF(q.BizDateTo.HasValue, e => e.CreatedAt <= q.BizDateTo!.Value);
- // 聚合到内存(数据量在可控范围内,避免复杂 GROUP BY 兼容性问题)
- var raw = await query
- .Select(e => new
- {
- e.ModuleCode,
- e.SceneCode,
- e.Severity,
- e.TimeoutFlag,
- e.Status
- })
- .ToListAsync();
- var byModule = raw
- .GroupBy(e => new { mc = e.ModuleCode ?? string.Empty, sc = e.SceneCode ?? string.Empty })
- .Select(g => new AdoS8ModuleSummaryItem
- {
- ModuleCode = g.Key.mc,
- ModuleLabel = S8ModuleCode.Label(g.Key.mc),
- SceneCode = g.Key.sc,
- SceneLabel = S8SceneCode.Label(g.Key.sc),
- Total = g.Count(),
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:red=SERIOUS / yellow=FOLLOW;green=0(异常表无正常桶)。
- Red = g.Count(e => S8SeverityCode.IsSerious(e.Severity)),
- Yellow = g.Count(e => S8SeverityCode.IsFollow(e.Severity)),
- Green = 0,
- Timeout = g.Count(e => e.TimeoutFlag && e.Status != "CLOSED")
- })
- // 按 S8ModuleCode.All 顺序排列
- .OrderBy(r => Array.IndexOf(S8ModuleCode.All, r.ModuleCode))
- .ToList();
- return new AdoS8MonitoringSummaryDto
- {
- Total = raw.Count,
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:red=SERIOUS / yellow=FOLLOW;green=0。
- Red = raw.Count(e => S8SeverityCode.IsSerious(e.Severity)),
- Yellow = raw.Count(e => S8SeverityCode.IsFollow(e.Severity)),
- Green = 0,
- Timeout = raw.Count(e => e.TimeoutFlag && e.Status != "CLOSED"),
- ByModule = byModule
- };
- }
- /// <summary>
- /// S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:Delivery 页近 N 日交付异常趋势。
- /// 口径:module_code IN (S1,S7) AND exception_type_code IN (ORDER_CHANGE / DELIVERY_DELAY / PENDING_SHIPMENT);
- /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
- /// </summary>
- public async Task<AdoS8DeliveryTrendDto> GetDeliveryTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
- {
- days = Math.Clamp(days, 1, 30);
- var today = DateTime.Today;
- var from = today.AddDays(-(days - 1));
- var toExclusive = today.AddDays(1);
- var deliveryModules = new[] { "S1", "S7" };
- var deliveryTypes = new[] { "ORDER_CHANGE", "DELIVERY_DELAY", "PENDING_SHIPMENT" };
- var rows = await _rep.AsQueryable()
- .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Where(e => deliveryModules.Contains(e.ModuleCode))
- .Where(e => e.ExceptionTypeCode != null && deliveryTypes.Contains(e.ExceptionTypeCode))
- .Where(e => e.CreatedAt >= from && e.CreatedAt < toExclusive)
- .Select(e => new { e.ExceptionTypeCode, e.CreatedAt })
- .ToListAsync();
- var byDate = rows
- .GroupBy(r => r.CreatedAt.Date)
- .ToDictionary(g => g.Key, g => g.ToList());
- var dayList = new List<AdoS8DeliveryTrendDayDto>(days);
- for (var i = 0; i < days; i++)
- {
- var d = from.AddDays(i);
- var bucket = byDate.TryGetValue(d, out var list) ? list : new();
- var oc = bucket.Count(r => r.ExceptionTypeCode == "ORDER_CHANGE");
- var dd = bucket.Count(r => r.ExceptionTypeCode == "DELIVERY_DELAY");
- var ps = bucket.Count(r => r.ExceptionTypeCode == "PENDING_SHIPMENT");
- dayList.Add(new AdoS8DeliveryTrendDayDto
- {
- Date = d.ToString("MM/dd"),
- RawDate = d.ToString("yyyy-MM-dd"),
- OrderChange = oc,
- DeliveryDelay = dd,
- PendingShipment = ps,
- Total = oc + dd + ps,
- });
- }
- var totalSum = dayList.Sum(d => d.Total);
- var peak = dayList.OrderByDescending(d => d.Total).FirstOrDefault();
- var todayDay = dayList.Last();
- var yesterday = dayList.Count >= 2 ? dayList[^2] : null;
- double? changeRate = (yesterday is null || yesterday.Total == 0)
- ? (double?)null
- : Math.Round((todayDay.Total - yesterday.Total) * 100.0 / yesterday.Total, 1);
- var summary = new AdoS8DeliveryTrendSummaryDto
- {
- PeakValue = peak?.Total ?? 0,
- PeakDate = (peak is null || peak.Total == 0) ? null : peak.RawDate,
- AvgValue = Math.Round(totalSum / (double)days, 2),
- TodayValue = todayDay.Total,
- TodayChangeRate = changeRate,
- };
- return new AdoS8DeliveryTrendDto { Days = dayList, Summary = summary };
- }
- /// <summary>
- /// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:Production 页近 N 日生产异常趋势。
- /// 口径:module_code IN (S2,S6) AND exception_type_code IN (EQUIP_FAULT / MFG_MATERIAL_ABNORMAL / MFG_QUALITY_ABNORMAL);
- /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
- /// </summary>
- public async Task<AdoS8ProductionTrendDto> GetProductionTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
- {
- days = Math.Clamp(days, 1, 30);
- var today = DateTime.Today;
- var from = today.AddDays(-(days - 1));
- var toExclusive = today.AddDays(1);
- var prodModules = new[] { "S2", "S6" };
- var prodTypes = new[] { "EQUIP_FAULT", "MFG_MATERIAL_ABNORMAL", "MFG_QUALITY_ABNORMAL" };
- var rows = await _rep.AsQueryable()
- .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Where(e => prodModules.Contains(e.ModuleCode))
- .Where(e => e.ExceptionTypeCode != null && prodTypes.Contains(e.ExceptionTypeCode))
- .Where(e => e.CreatedAt >= from && e.CreatedAt < toExclusive)
- .Select(e => new { e.ExceptionTypeCode, e.CreatedAt })
- .ToListAsync();
- var byDate = rows.GroupBy(r => r.CreatedAt.Date).ToDictionary(g => g.Key, g => g.ToList());
- var dayList = new List<AdoS8ProductionTrendDayDto>(days);
- for (var i = 0; i < days; i++)
- {
- var d = from.AddDays(i);
- var bucket = byDate.TryGetValue(d, out var list) ? list : new();
- var ef = bucket.Count(r => r.ExceptionTypeCode == "EQUIP_FAULT");
- var mf = bucket.Count(r => r.ExceptionTypeCode == "MFG_MATERIAL_ABNORMAL");
- var qf = bucket.Count(r => r.ExceptionTypeCode == "MFG_QUALITY_ABNORMAL");
- dayList.Add(new AdoS8ProductionTrendDayDto
- {
- Date = d.ToString("MM/dd"),
- RawDate = d.ToString("yyyy-MM-dd"),
- EquipmentFault = ef,
- MaterialFault = mf,
- QualityFault = qf,
- Total = ef + mf + qf,
- });
- }
- var totalSum = dayList.Sum(d => d.Total);
- var peak = dayList.OrderByDescending(d => d.Total).FirstOrDefault();
- var todayDay = dayList.Last();
- var yesterday = dayList.Count >= 2 ? dayList[^2] : null;
- double? changeRate = (yesterday is null || yesterday.Total == 0)
- ? (double?)null
- : Math.Round((todayDay.Total - yesterday.Total) * 100.0 / yesterday.Total, 1);
- var summary = new AdoS8DeliveryTrendSummaryDto
- {
- PeakValue = peak?.Total ?? 0,
- PeakDate = (peak is null || peak.Total == 0) ? null : peak.RawDate,
- AvgValue = Math.Round(totalSum / (double)days, 2),
- TodayValue = todayDay.Total,
- TodayChangeRate = changeRate,
- };
- return new AdoS8ProductionTrendDto { Days = dayList, Summary = summary };
- }
- /// <summary>
- /// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:Supply 页近 N 日供应异常趋势。
- /// 口径:module_code IN (S3,S4,S5) AND exception_type_code IN 7 类(SUPPLIER_ETA_ISSUE / SUPPLIER_SHIP_ISSUE
- /// / WAREHOUSE_RECEIPT_ABNORMAL / IQC_ISSUE / WH_PUTAWAY_ISSUE / WORK_ORDER_KITTING_ABNORMAL / WORK_ORDER_ISSUE_ABNORMAL);
- /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
- /// </summary>
- public async Task<AdoS8SupplyTrendDto> GetSupplyTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
- {
- days = Math.Clamp(days, 1, 30);
- var today = DateTime.Today;
- var from = today.AddDays(-(days - 1));
- var toExclusive = today.AddDays(1);
- var supplyModules = new[] { "S3", "S4", "S5" };
- var supplyTypes = new[]
- {
- "SUPPLIER_ETA_ISSUE", "SUPPLIER_SHIP_ISSUE", "WAREHOUSE_RECEIPT_ABNORMAL",
- "IQC_ISSUE", "WH_PUTAWAY_ISSUE", "WORK_ORDER_KITTING_ABNORMAL", "WORK_ORDER_ISSUE_ABNORMAL"
- };
- var rows = await _rep.AsQueryable()
- .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Where(e => supplyModules.Contains(e.ModuleCode))
- .Where(e => e.ExceptionTypeCode != null && supplyTypes.Contains(e.ExceptionTypeCode))
- .Where(e => e.CreatedAt >= from && e.CreatedAt < toExclusive)
- .Select(e => new { e.ExceptionTypeCode, e.CreatedAt })
- .ToListAsync();
- var byDate = rows.GroupBy(r => r.CreatedAt.Date).ToDictionary(g => g.Key, g => g.ToList());
- var dayList = new List<AdoS8SupplyTrendDayDto>(days);
- for (var i = 0; i < days; i++)
- {
- var d = from.AddDays(i);
- var bucket = byDate.TryGetValue(d, out var list) ? list : new();
- var s1 = bucket.Count(r => r.ExceptionTypeCode == "SUPPLIER_ETA_ISSUE");
- var s2 = bucket.Count(r => r.ExceptionTypeCode == "SUPPLIER_SHIP_ISSUE");
- var s3 = bucket.Count(r => r.ExceptionTypeCode == "WAREHOUSE_RECEIPT_ABNORMAL");
- var s4 = bucket.Count(r => r.ExceptionTypeCode == "IQC_ISSUE");
- var s5 = bucket.Count(r => r.ExceptionTypeCode == "WH_PUTAWAY_ISSUE");
- var s6 = bucket.Count(r => r.ExceptionTypeCode == "WORK_ORDER_KITTING_ABNORMAL");
- var s7 = bucket.Count(r => r.ExceptionTypeCode == "WORK_ORDER_ISSUE_ABNORMAL");
- dayList.Add(new AdoS8SupplyTrendDayDto
- {
- Date = d.ToString("MM/dd"),
- RawDate = d.ToString("yyyy-MM-dd"),
- SupplierEtaIssue = s1,
- SupplierShipIssue = s2,
- WarehouseReceiptAbnormal = s3,
- IqcIssue = s4,
- WarehousePutawayIssue = s5,
- WorkOrderKittingAbnormal = s6,
- WorkOrderIssueAbnormal = s7,
- Total = s1 + s2 + s3 + s4 + s5 + s6 + s7,
- });
- }
- var totalSum = dayList.Sum(d => d.Total);
- var peak = dayList.OrderByDescending(d => d.Total).FirstOrDefault();
- var todayDay = dayList.Last();
- var yesterday = dayList.Count >= 2 ? dayList[^2] : null;
- double? changeRate = (yesterday is null || yesterday.Total == 0)
- ? (double?)null
- : Math.Round((todayDay.Total - yesterday.Total) * 100.0 / yesterday.Total, 1);
- var summary = new AdoS8DeliveryTrendSummaryDto
- {
- PeakValue = peak?.Total ?? 0,
- PeakDate = (peak is null || peak.Total == 0) ? null : peak.RawDate,
- AvgValue = Math.Round(totalSum / (double)days, 2),
- TodayValue = todayDay.Total,
- TodayChangeRate = changeRate,
- };
- return new AdoS8SupplyTrendDto { Days = dayList, Summary = summary };
- }
- /// <summary>
- /// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:专题页右侧类型卡按 (domain, window) 统一聚合。
- /// 单一时间窗口下 total / open / closed / avgProcessHours / closeRate 共用同一分母。
- /// 公共过滤:tenant/factory + is_deleted=0 + module_code IN S1-S7 + exception_type_code IN domain 类型集 + created_at IN window。
- /// </summary>
- public async Task<AdoS8DomainTypeMetricsDto> GetDomainTypeMetricsAsync(string domain, string window, long tenantId = 1, long factoryId = 1)
- {
- var d = (domain ?? string.Empty).Trim().ToUpperInvariant();
- var w = (window ?? string.Empty).Trim().ToUpperInvariant();
- if (w != "LAST_24H" && w != "LAST_7D") w = "LAST_24H";
- (string key, string label, string typeCode)[] specs = d switch
- {
- "DELIVERY" => new (string, string, string)[]
- {
- ("order-change", "订单变更", "ORDER_CHANGE"),
- ("delivery-delay", "交期延迟", "DELIVERY_DELAY"),
- ("stock-pending", "入库待发", "PENDING_SHIPMENT"),
- },
- "PRODUCTION" => new (string, string, string)[]
- {
- ("equipment-fault", "设备异常", "EQUIP_FAULT"),
- ("material-fault", "物料异常", "MFG_MATERIAL_ABNORMAL"),
- ("quality-fault", "质量异常", "MFG_QUALITY_ABNORMAL"),
- },
- "SUPPLY" => new (string, string, string)[]
- {
- ("supplier-reply-delay", "供应商回复交期异常", "SUPPLIER_ETA_ISSUE"),
- ("supplier-ship-fault", "供应商发货异常", "SUPPLIER_SHIP_ISSUE"),
- ("warehouse-receipt", "仓库收货异常", "WAREHOUSE_RECEIPT_ABNORMAL"),
- ("iqc-inspection", "IQC 检验异常", "IQC_ISSUE"),
- ("warehouse-shelving", "仓库上架入库异常", "WH_PUTAWAY_ISSUE"),
- ("work-order-prepare", "仓库工单备料异常", "WORK_ORDER_KITTING_ABNORMAL"),
- ("work-order-issue", "仓库工单发料异常", "WORK_ORDER_ISSUE_ABNORMAL"),
- },
- _ => Array.Empty<(string, string, string)>(),
- };
- var typeCodes = specs.Select(s => s.typeCode).ToArray();
- if (typeCodes.Length == 0)
- {
- return new AdoS8DomainTypeMetricsDto { Domain = d, Window = w, Total = 0, Items = new() };
- }
- DateTime from, to;
- if (w == "LAST_7D")
- {
- // 与 trend 接口保持一致:[today-6, today+1)
- var today = DateTime.Today;
- from = today.AddDays(-6);
- to = today.AddDays(1);
- }
- else
- {
- // LAST_24H:滑动窗口 NOW()-24h ~ NOW()
- var now = DateTime.Now;
- from = now.AddHours(-24);
- to = now.AddMinutes(1);
- }
- var rows = await _rep.AsQueryable()
- .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Where(e => S8ModuleCode.All.Contains(e.ModuleCode))
- .Where(e => e.ExceptionTypeCode != null && typeCodes.Contains(e.ExceptionTypeCode))
- .Where(e => e.CreatedAt >= from && e.CreatedAt < to)
- .Select(e => new { e.ExceptionTypeCode, e.Status, e.CreatedAt, e.ClosedAt })
- .ToListAsync();
- var byType = rows.GroupBy(r => r.ExceptionTypeCode!).ToDictionary(g => g.Key, g => g.ToList());
- var items = specs.Select(s =>
- {
- var bucket = byType.TryGetValue(s.typeCode, out var list) ? list : new();
- var total = bucket.Count;
- var closedCount = bucket.Count(r => r.Status == "CLOSED");
- var openCount = total - closedCount;
- double? avgHours = null;
- var closedSamples = bucket.Where(r => r.ClosedAt.HasValue).ToList();
- if (closedSamples.Count > 0)
- {
- avgHours = Math.Round(closedSamples.Average(r => (r.ClosedAt!.Value - r.CreatedAt).TotalHours), 1);
- }
- double? closeRate = total == 0 ? null : Math.Round(closedCount * 100.0 / total, 1);
- return new AdoS8DomainTypeMetricItemDto
- {
- Key = s.key,
- Label = s.label,
- TypeCode = s.typeCode,
- Total = total,
- OpenCount = openCount,
- ClosedCount = closedCount,
- AvgProcessHours = avgHours,
- CloseRate = closeRate,
- };
- }).ToList();
- return new AdoS8DomainTypeMetricsDto
- {
- Domain = d,
- Window = w,
- Total = items.Sum(i => i.Total),
- Items = items,
- };
- }
- }
|