using Admin.NET.Plugin.AiDOP.Dto.S8; using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing; using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; namespace Admin.NET.Plugin.AiDOP.Service.S8; public class S8ExceptionService : ITransient { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _deptRep; private readonly SqlSugarRepository _empRep; private readonly SqlSugarRepository _sysUserRep; public S8ExceptionService( SqlSugarRepository rep, SqlSugarRepository deptRep, SqlSugarRepository empRep, SqlSugarRepository sysUserRep) { _rep = rep; _deptRep = deptRep; _empRep = empRep; _sysUserRep = sysUserRep; } public async Task<(int total, List list)> GetPagedAsync(AdoS8ExceptionQueryDto q) { (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" }; var query = _rep.Context.Queryable() .LeftJoin((e, sc) => e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode) .Where((e, sc) => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(q.Status), (e, sc) => e.Status == q.Status) .WhereIF(string.IsNullOrWhiteSpace(q.Status) && q.StatusBucket == "pending", (e, sc) => pendingStatuses.Contains(e.Status)) .WhereIF(!string.IsNullOrWhiteSpace(q.Severity), (e, sc) => e.Severity == q.Severity) .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), (e, sc) => e.SceneCode == q.SceneCode) .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), (e, sc) => e.ModuleCode == q.ModuleCode) .WhereIF(q.DeptId.HasValue, (e, sc) => e.ResponsibleDeptId == q.DeptId!.Value || e.OccurrenceDeptId == q.DeptId!.Value) .WhereIF(q.TimeoutFlag.HasValue, (e, sc) => e.TimeoutFlag == q.TimeoutFlag!.Value) .WhereIF(q.BeginTime.HasValue, (e, sc) => e.CreatedAt >= q.BeginTime!.Value) .WhereIF(q.EndTime.HasValue, (e, sc) => e.CreatedAt <= q.EndTime!.Value) .WhereIF(!string.IsNullOrWhiteSpace(q.ProcessNodeCode), (e, sc) => e.ProcessNodeCode == q.ProcessNodeCode) .WhereIF(!string.IsNullOrWhiteSpace(q.RelatedObjectCode), (e, sc) => e.RelatedObjectCode == q.RelatedObjectCode) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), (e, sc) => e.Title.Contains(q.Keyword!) || e.ExceptionCode.Contains(q.Keyword!)); var total = await query.CountAsync(); var list = await query .OrderBy((e, sc) => e.CreatedAt, OrderByType.Desc) .Select((e, sc) => new AdoS8ExceptionListItemDto { Id = e.Id, FactoryId = e.FactoryId, ExceptionCode = e.ExceptionCode, Title = e.Title, Status = e.Status, Severity = e.Severity, PriorityScore = e.PriorityScore, PriorityLevel = e.PriorityLevel, SceneCode = e.SceneCode, SceneName = sc.SceneName, ResponsibleDeptId = e.ResponsibleDeptId, AssigneeId = e.AssigneeId, SlaDeadline = e.SlaDeadline, TimeoutFlag = e.TimeoutFlag, CreatedAt = e.CreatedAt, ClosedAt = e.ClosedAt }) .ToPageListAsync(q.Page, q.PageSize); foreach (var r in list) { r.StatusLabel = S8Labels.StatusLabel(r.Status); r.SeverityLabel = S8Labels.SeverityLabel(r.Severity); } await FillDisplayNamesAsync(list); return (total, list); } public async Task GetFilterOptionsAsync(long tenantId, long factoryId) { var scenes = await _rep.Context.Queryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled) .OrderBy(x => x.SortNo) .Select(x => new { value = x.SceneCode, label = x.SceneName }) .ToListAsync(); var departments = await _deptRep.AsQueryable() .Where(x => x.FactoryRefId == factoryId) .OrderBy(x => x.Department) .Take(500) .Select(x => new { value = x.Id, label = x.Descr ?? x.Department }) .ToListAsync(); return new { statuses = S8Labels.StatusOptions(), severities = S8Labels.SeverityOptions(), scenes, departments }; } public async Task GetDetailAsync(long id, long tenantId, long factoryId) { var rows = await _rep.Context.Queryable() .LeftJoin((e, sc) => e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode) .Where((e, sc) => e.Id == id && e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted) .Select((e, sc) => new AdoS8ExceptionDetailDto { Id = e.Id, FactoryId = e.FactoryId, ExceptionCode = e.ExceptionCode, Title = e.Title, Description = e.Description, Status = e.Status, Severity = e.Severity, PriorityScore = e.PriorityScore, PriorityLevel = e.PriorityLevel, SceneCode = e.SceneCode, SceneName = sc.SceneName, SourceType = e.SourceType, OccurrenceDeptId = e.OccurrenceDeptId, ResponsibleDeptId = e.ResponsibleDeptId, ResponsibleGroupId = e.ResponsibleGroupId, AssigneeId = e.AssigneeId, ReporterId = e.ReporterId, SlaDeadline = e.SlaDeadline, TimeoutFlag = e.TimeoutFlag, CreatedAt = e.CreatedAt, ClosedAt = e.ClosedAt, AssignedAt = e.AssignedAt, UpdatedAt = e.UpdatedAt, ActiveFlowInstanceId = e.ActiveFlowInstanceId, ActiveFlowBizType = e.ActiveFlowBizType, VerifierId = e.VerifierId, VerificationAssignedAt = e.VerificationAssignedAt, VerifiedAt = e.VerifiedAt, VerificationResult = e.VerificationResult, VerificationRemark = e.VerificationRemark, SourceRuleId = e.SourceRuleId, RelatedObjectCode = e.RelatedObjectCode, DedupKey = e.DedupKey, LastDetectedAt = e.LastDetectedAt, RecoveredAt = e.RecoveredAt, SourceRuleCode = e.SourceRuleCode, SourceObjectType = e.SourceObjectType, SourceObjectId = e.SourceObjectId, }) .Take(1) .ToListAsync(); if (rows.Count == 0) return null; var d = rows[0]; d.StatusLabel = S8Labels.StatusLabel(d.Status); d.SeverityLabel = S8Labels.SeverityLabel(d.Severity); await FillDisplayNamesAsync(new[] { d }); return d; } private async Task FillDisplayNamesAsync(IEnumerable rows) { var list = rows.ToList(); if (list.Count == 0) return; var deptIds = list .Select(x => x.ResponsibleDeptId) .Concat(list.OfType().Select(x => x.OccurrenceDeptId)) .Where(x => x > 0) .Distinct() .ToList(); var empIds = list .Select(x => x.AssigneeId ?? 0L) .Concat(list.OfType().Select(x => x.ReporterId ?? 0L)) .Concat(list.OfType().Select(x => x.VerifierId ?? 0L)) .Where(x => x > 0) .Distinct() .ToList(); var deptMap = deptIds.Count == 0 ? new Dictionary() : (await _deptRep.AsQueryable() .Where(x => deptIds.Contains(x.Id)) .Select(x => new { x.Id, Name = x.Descr ?? x.Department }) .ToListAsync()) .ToDictionary(x => x.Id, x => x.Name); var empMap = empIds.Count == 0 ? new Dictionary() : (await _empRep.AsQueryable() .Where(x => empIds.Contains(x.Id)) .Select(x => new { x.Id, Name = x.Name ?? x.Employee }) .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() .Select(x => x.ReporterId ?? 0L) .Where(x => x > 0) .Distinct() .ToList(); var reporterUserMap = reporterUserIds.Count == 0 ? new Dictionary() : (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) { 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; } } } /// /// S2-EW:debug-only 软删 RelatedObjectCode 以 "TEST_G09_" 开头的异常。 /// 调用方仅限 ,由其 _debugEndpointEnabled 守门。 /// 实现纪律: /// - 仅 SET IsDeleted=true,不动业务字段(status/closed_at/source_rule_id 等)。 /// - 前缀字面量硬编码,不接受外部 prefix;防止前缀注入。 /// - 不联动 timeline / decision / evidence;业务读路径以父 IsDeleted 屏蔽。 /// public async Task SoftDeleteTestPrefixAsync(long tenantId, long factoryId) { const string prefix = "TEST_G09_"; var affected = await _rep.AsUpdateable() .SetColumns(e => new AdoS8Exception { IsDeleted = true }) .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted && e.RelatedObjectCode != null && e.RelatedObjectCode.StartsWith(prefix)) .ExecuteCommandAsync(); System.Diagnostics.Trace.TraceWarning( $"[S2-EW] soft-deleted {affected} TEST_G09_ exceptions, tenant={tenantId} factory={factoryId}"); return affected; } }