Bladeren bron

fix(s8): align department display and timeout read metrics

YY968XX 4 weken geleden
bovenliggende
commit
a7bd3561be

+ 4 - 0
Web/src/views/aidop/s8/api/s8ExceptionApi.ts

@@ -23,6 +23,10 @@ export interface S8ExceptionRow {
 	priorityLevel: string;
 	sceneCode: string;
 	sceneName?: string | null;
+	responsibleDeptId?: number | null;
+	responsibleDeptName?: string | null;
+	occurrenceDeptId?: number | null;
+	occurrenceDeptName?: string | null;
 	assigneeId?: number | null;
 	slaDeadline?: string | null;
 	timeoutFlag: boolean;

+ 13 - 0
Web/src/views/aidop/s8/exceptions/S8ExceptionListPage.vue

@@ -71,6 +71,12 @@
 			<el-table-column prop="statusLabel" label="状态" width="100" />
 			<el-table-column prop="severityLabel" label="严重度" width="90" />
 			<el-table-column prop="priorityLevel" label="优先级" width="90" />
+			<el-table-column label="发生部门" width="130" show-overflow-tooltip>
+				<template #default="{ row }">{{ deptDisplay(row.occurrenceDeptName, row.occurrenceDeptId) }}</template>
+			</el-table-column>
+			<el-table-column label="处理部门" width="130" show-overflow-tooltip>
+				<template #default="{ row }">{{ deptDisplay(row.responsibleDeptName, row.responsibleDeptId) }}</template>
+			</el-table-column>
 			<el-table-column prop="timeoutFlag" label="超时" width="70" align="center">
 				<template #default="{ row }">
 					<el-tag :type="row.timeoutFlag ? 'danger' : 'info'" size="small">{{ row.timeoutFlag ? '是' : '否' }}</el-tag>
@@ -158,6 +164,13 @@ const query = reactive({
 	factoryId: 1,
 });
 
+// S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-2):部门展示 fallback —— 名→部门ID:{id}→未归属。后端已按 factory_ref_id 水合。
+function deptDisplay(name?: string | null, id?: number | null) {
+	if (name && name.trim()) return name;
+	if (id && id > 0) return `部门ID:${id}`;
+	return '未归属';
+}
+
 function ruleTypeTagType(t: string | null | undefined) {
 	switch (t) {
 		case 'OUT_OF_RANGE':

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs

@@ -46,6 +46,8 @@ public class AdoS8ExceptionListItemDto
     public string? SceneName { get; set; }
     public long ResponsibleDeptId { get; set; }
     public string? ResponsibleDeptName { get; set; }
+    public long OccurrenceDeptId { get; set; }
+    public string? OccurrenceDeptName { get; set; }
     public long? AssigneeId { get; set; }
     public string? AssigneeName { get; set; }
     public DateTime? SlaDeadline { get; set; }
@@ -66,8 +68,6 @@ public class AdoS8ExceptionDetailDto : AdoS8ExceptionListItemDto
 {
     public string? Description { get; set; }
     public string SourceType { get; set; } = string.Empty;
-    public long OccurrenceDeptId { get; set; }
-    public string? OccurrenceDeptName { get; set; }
     public long? ResponsibleGroupId { get; set; }
     public long? ReporterId { get; set; }
     public string? ReporterName { get; set; }

+ 11 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardService.cs

@@ -45,7 +45,8 @@ public class S8DashboardService : ITransient
         var total      = await q.CountAsync();
         var pending    = await q.CountAsync(x => x.Status == "NEW" || x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS" || x.Status == "PENDING_VERIFICATION" || x.Status == "REJECTED");
         var inProgress = await q.CountAsync(x => x.Status == "IN_PROGRESS");
-        var timeout    = await q.CountAsync(x => x.TimeoutFlag);
+        // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-1):timeout 统计排除 CLOSED,与 S8MonitoringService 口径一致。
+        var timeout    = await q.CountAsync(x => x.TimeoutFlag && x.Status != "CLOSED");
         var closed     = await q.CountAsync(x => x.Status == "CLOSED");
         var todayNew   = await q.CountAsync(x => x.CreatedAt >= DateTime.Today);
         // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:critical 字段名保留以维持外部 KPI;语义切为「严重」(SERIOUS)。
@@ -104,8 +105,9 @@ public class S8DashboardService : ITransient
         var allDeptIds = list.Select(x => x.ResponsibleDeptId)
             .Concat(list.Select(x => x.OccurrenceDeptId))
             .Distinct().ToList();
+        // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-3):部门水合加 factory_ref_id 约束,避免跨 factory 同 RecID 错位。
         var deptMap = await _deptRep.AsQueryable()
-            .Where(d => allDeptIds.Contains(d.Id))
+            .Where(d => allDeptIds.Contains(d.Id) && d.FactoryRefId == factoryId)
             .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
             .ToListAsync();
         var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString());
@@ -182,8 +184,9 @@ public class S8DashboardService : ITransient
             .ToListAsync();
 
         var deptIds = list.Select(x => x.ResponsibleDeptId).Distinct().ToList();
+        // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-3):部门水合加 factory_ref_id 约束。
         var deptMap = await _deptRep.AsQueryable()
-            .Where(d => deptIds.Contains(d.Id))
+            .Where(d => deptIds.Contains(d.Id) && d.FactoryRefId == factoryId)
             .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
             .ToListAsync();
         var deptDict = deptMap.ToDictionary(d => d.Id, d => d.Name ?? d.Id.ToString());
@@ -196,7 +199,8 @@ public class S8DashboardService : ITransient
                 deptName = deptDict.GetValueOrDefault(g.Key, g.Key.ToString()),
                 pending    = g.Count(x => pendingStatuses.Contains(x.Status)),
                 inProgress = g.Count(x => x.Status == "IN_PROGRESS"),
-                timeout    = g.Count(x => x.TimeoutFlag),
+                // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-1):timeout 排除 CLOSED,与 GetSummaryAsync 口径一致。
+                timeout    = g.Count(x => x.TimeoutFlag && x.Status != "CLOSED"),
                 total      = g.Count(),
             })
             .OrderByDescending(x => x.pending)
@@ -259,8 +263,10 @@ public class S8DashboardService : ITransient
         else if (dim is "occDept" or "respDept")
         {
             var ids = grouped.Select(g => long.TryParse(g.Key, out var id) ? id : 0).ToList();
+            // S8-DEPT-DISPLAY-CONSISTENCY-1-FOLLOWUP-1(P0-A-3 收尾):部门水合加 factory_ref_id 约束,
+            // 与 GetDistributionsAsync / GetDeptBacklogAsync 同口径,避免跨 factory 同名/同 RecID 错位。
             var depts = await _deptRep.AsQueryable()
-                .Where(d => ids.Contains(d.Id))
+                .Where(d => ids.Contains(d.Id) && d.FactoryRefId == factoryId)
                 .Select(d => new { d.Id, Name = d.Descr ?? d.Department })
                 .ToListAsync();
             nameMap = depts.ToDictionary(d => d.Id.ToString(), d => d.Name ?? d.Id.ToString());

+ 21 - 13
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs

@@ -80,6 +80,7 @@ public class S8ExceptionService : ITransient
                 SceneCode = e.SceneCode,
                 SceneName = sc.SceneName,
                 ResponsibleDeptId = e.ResponsibleDeptId,
+                OccurrenceDeptId = e.OccurrenceDeptId,
                 AssigneeId = e.AssigneeId,
                 SlaDeadline = e.SlaDeadline,
                 TimeoutFlag = e.TimeoutFlag,
@@ -102,7 +103,7 @@ public class S8ExceptionService : ITransient
             r.SeverityLabel = S8Labels.SeverityLabel(r.Severity);
         }
 
-        await FillDisplayNamesAsync(list);
+        await FillDisplayNamesAsync(list, q.FactoryId);
         return (total, list);
     }
 
@@ -187,18 +188,20 @@ public class S8ExceptionService : ITransient
         var d = rows[0];
         d.StatusLabel = S8Labels.StatusLabel(d.Status);
         d.SeverityLabel = S8Labels.SeverityLabel(d.Severity);
-        await FillDisplayNamesAsync(new[] { d });
+        await FillDisplayNamesAsync(new[] { d }, factoryId);
         return d;
     }
 
-    private async Task FillDisplayNamesAsync(IEnumerable<AdoS8ExceptionListItemDto> rows)
+    // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-2/A-3):list 行也水合 OccurrenceDeptName;
+    // 部门查询加 factory_ref_id 二次约束,避免跨 factory 同名 / 同 RecID 错位。
+    private async Task FillDisplayNamesAsync(IEnumerable<AdoS8ExceptionListItemDto> rows, long factoryId)
     {
         var list = rows.ToList();
         if (list.Count == 0) return;
 
         var deptIds = list
             .Select(x => x.ResponsibleDeptId)
-            .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.OccurrenceDeptId))
+            .Concat(list.Select(x => x.OccurrenceDeptId))
             .Where(x => x > 0)
             .Distinct()
             .ToList();
@@ -214,7 +217,7 @@ public class S8ExceptionService : ITransient
         var deptMap = deptIds.Count == 0
             ? new Dictionary<long, string>()
             : (await _deptRep.AsQueryable()
-                .Where(x => deptIds.Contains(x.Id))
+                .Where(x => deptIds.Contains(x.Id) && x.FactoryRefId == factoryId)
                 .Select(x => new { x.Id, Name = x.Descr ?? x.Department })
                 .ToListAsync())
                 .ToDictionary(x => x.Id, x => x.Name);
@@ -251,24 +254,29 @@ public class S8ExceptionService : ITransient
 
         foreach (var row in list)
         {
+            row.ResponsibleDeptName = ResolveDeptName(row.ResponsibleDeptId, deptMap);
+            row.OccurrenceDeptName = ResolveDeptName(row.OccurrenceDeptId, deptMap);
+            row.AssigneeName = row.AssigneeId.HasValue ? empMap.GetValueOrDefault(row.AssigneeId.Value) : null;
+
             if (row is AdoS8ExceptionDetailDto detail)
             {
-                detail.ResponsibleDeptName = deptMap.GetValueOrDefault(detail.ResponsibleDeptId);
-                detail.OccurrenceDeptName = deptMap.GetValueOrDefault(detail.OccurrenceDeptId);
-                detail.AssigneeName = detail.AssigneeId.HasValue ? empMap.GetValueOrDefault(detail.AssigneeId.Value) : null;
                 detail.ReporterName = detail.ReporterId.HasValue
                     ? (reporterUserMap.GetValueOrDefault(detail.ReporterId.Value) ?? empMap.GetValueOrDefault(detail.ReporterId.Value))
                     : null;
                 detail.VerifierName = detail.VerifierId.HasValue ? empMap.GetValueOrDefault(detail.VerifierId.Value) : null;
             }
-            else
-            {
-                row.ResponsibleDeptName = deptMap.GetValueOrDefault(row.ResponsibleDeptId);
-                row.AssigneeName = row.AssigneeId.HasValue ? empMap.GetValueOrDefault(row.AssigneeId.Value) : null;
-            }
         }
     }
 
+    // S8-DEPT-DISPLAY-CONSISTENCY-1(P0-A-3):dept fallback —— 0/缺失=未归属,查不到名=部门ID:{id}。
+    private static string? ResolveDeptName(long deptId, Dictionary<long, string> deptMap)
+    {
+        if (deptId <= 0) return "未归属";
+        return deptMap.TryGetValue(deptId, out var name) && !string.IsNullOrWhiteSpace(name)
+            ? name
+            : $"部门ID:{deptId}";
+    }
+
     /// <summary>
     /// S2-EW:debug-only 软删 RelatedObjectCode 以 "TEST_G09_" 开头的异常。
     /// 调用方仅限 <see cref="Controllers.S8.AdoS8WatchDebugController"/>,由其 _debugEndpointEnabled 守门。

+ 2 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MonitoringService.cs

@@ -106,10 +106,11 @@ public class S8MonitoringService : ITransient
         // 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))
+                    .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);