Bladeren bron

fix(s8): harden manual report metadata and severity validation

Manual report creation trusted the front-end for reporterId, leaving the
column null in practice, and accepted any severity string which let
"P3" leak into the column whose legal values are CRITICAL/HIGH/MEDIUM/LOW.

Stamp ReporterId / timeline OperatorId from UserManager.UserId server-
side, validate Severity against an explicit whitelist, and hydrate
reporter names through SysUser (falling back to EmployeeMaster for
historical rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YY968XX 3 maanden geleden
bovenliggende
commit
88ba558716

+ 25 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs

@@ -12,15 +12,18 @@ public class S8ExceptionService : ITransient
     private readonly SqlSugarRepository<AdoS8Exception> _rep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
 
     public S8ExceptionService(
         SqlSugarRepository<AdoS8Exception> rep,
         SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
-        SqlSugarRepository<AdoS0EmployeeMaster> empRep)
+        SqlSugarRepository<AdoS0EmployeeMaster> empRep,
+        SqlSugarRepository<SysUser> sysUserRep)
     {
         _rep = rep;
         _deptRep = deptRep;
         _empRep = empRep;
+        _sysUserRep = sysUserRep;
     }
 
     public async Task<(int total, List<AdoS8ExceptionListItemDto> list)> GetPagedAsync(AdoS8ExceptionQueryDto q)
@@ -192,6 +195,24 @@ public class S8ExceptionService : ITransient
                 .ToListAsync())
                 .ToDictionary(x => x.Id, x => x.Name);
 
+        // ReporterId 现在记录 sysUser.Id(OBS-S8-REPORT-REPORTER-NULL-001 修复后由服务端 JWT 兜底),
+        // 与 employee.Id 不在同一命名空间,需要单独查 SysUser 取 RealName/Account;
+        // 同时保留对历史 reporter=employee.Id 数据的兼容(empMap fallback)。
+        var reporterUserIds = list.OfType<AdoS8ExceptionDetailDto>()
+            .Select(x => x.ReporterId ?? 0L)
+            .Where(x => x > 0)
+            .Distinct()
+            .ToList();
+        var reporterUserMap = reporterUserIds.Count == 0
+            ? new Dictionary<long, string>()
+            : (await _sysUserRep.AsQueryable().ClearFilter()
+                .Where(u => reporterUserIds.Contains(u.Id))
+                .Select(u => new { u.Id, u.RealName, u.Account })
+                .ToListAsync())
+                .ToDictionary(
+                    u => u.Id,
+                    u => !string.IsNullOrWhiteSpace(u.RealName) ? u.RealName : u.Account);
+
         foreach (var row in list)
         {
             if (row is AdoS8ExceptionDetailDto detail)
@@ -199,7 +220,9 @@ public class S8ExceptionService : ITransient
                 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 ? empMap.GetValueOrDefault(detail.ReporterId.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

+ 21 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ManualReportService.cs

@@ -8,12 +8,19 @@ namespace Admin.NET.Plugin.AiDOP.Service.S8;
 
 public class S8ManualReportService : ITransient
 {
+    // 合法严重度白名单(与 GetFormOptionsAsync.severities 同源);前端/后端默认值 MEDIUM。
+    private static readonly HashSet<string> AllowedSeverities = new(StringComparer.Ordinal)
+    {
+        "CRITICAL", "HIGH", "MEDIUM", "LOW"
+    };
+
     private readonly SqlSugarRepository<AdoS8Exception> _rep;
     private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
     private readonly SqlSugarRepository<AdoS8Evidence> _evidenceRep;
     private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
     private readonly SqlSugarRepository<AdoS0LineMaster> _lineRep;
+    private readonly UserManager _userManager;
 
     public S8ManualReportService(
         SqlSugarRepository<AdoS8Exception> rep,
@@ -21,7 +28,8 @@ public class S8ManualReportService : ITransient
         SqlSugarRepository<AdoS8Evidence> evidenceRep,
         SqlSugarRepository<AdoS8SceneConfig> sceneRep,
         SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
-        SqlSugarRepository<AdoS0LineMaster> lineRep)
+        SqlSugarRepository<AdoS0LineMaster> lineRep,
+        UserManager userManager)
     {
         _rep = rep;
         _timelineRep = timelineRep;
@@ -29,6 +37,7 @@ public class S8ManualReportService : ITransient
         _sceneRep = sceneRep;
         _deptRep = deptRep;
         _lineRep = lineRep;
+        _userManager = userManager;
     }
 
     public async Task<object> GetFormOptionsAsync(long tenantId, long factoryId)
@@ -72,6 +81,14 @@ public class S8ManualReportService : ITransient
         if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
         if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
 
+        // 严重度白名单校验:空值兜底为 MEDIUM;非法值直接拒绝,避免 P3 等错位写入。
+        var severity = string.IsNullOrWhiteSpace(dto.Severity) ? "MEDIUM" : dto.Severity.Trim();
+        if (!AllowedSeverities.Contains(severity))
+            throw new S8BizException($"严重度 {severity} 非法,仅允许 CRITICAL/HIGH/MEDIUM/LOW");
+
+        // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
+        var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
+
         var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
         var entity = new AdoS8Exception
         {
@@ -83,12 +100,12 @@ public class S8ManualReportService : ITransient
             SceneCode = dto.SceneCode.Trim(),
             SourceType = "MANUAL",
             Status = "NEW",
-            Severity = string.IsNullOrWhiteSpace(dto.Severity) ? "MEDIUM" : dto.Severity,
+            Severity = severity,
             PriorityScore = 0,
             PriorityLevel = "P3",
             OccurrenceDeptId = dto.OccurrenceDeptId,
             ResponsibleDeptId = dto.ResponsibleDeptId,
-            ReporterId = dto.ReporterId,
+            ReporterId = currentUserId,
             CreatedAt = DateTime.Now,
             IsDeleted = false
         };
@@ -103,7 +120,7 @@ public class S8ManualReportService : ITransient
                 ActionLabel = "创建",
                 FromStatus = null,
                 ToStatus = "NEW",
-                OperatorId = dto.ReporterId,
+                OperatorId = currentUserId,
                 ActionRemark = "主动提报",
                 CreatedAt = DateTime.Now
             });