| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- 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<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<SysUser> sysUserRep)
- {
- _rep = rep;
- _deptRep = deptRep;
- _empRep = empRep;
- _sysUserRep = sysUserRep;
- }
- public async Task<(int total, List<AdoS8ExceptionListItemDto> list)> GetPagedAsync(AdoS8ExceptionQueryDto q)
- {
- (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
- var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" };
- var includeUnclassified = q.IncludeUnclassified == true;
- var query = _rep.Context.Queryable<AdoS8Exception>()
- .LeftJoin<AdoS8SceneConfig>((e, sc) =>
- e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
- .LeftJoin<AdoS8WatchRule>((e, sc, wr) =>
- e.TenantId == wr.TenantId && e.FactoryId == wr.FactoryId && e.SourceRuleCode == wr.RuleCode)
- .Where((e, sc, wr) => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
- .WhereIF(!includeUnclassified, (e, sc, wr) => !SqlFunc.IsNullOrEmpty(e.ExceptionTypeCode))
- .WhereIF(!string.IsNullOrWhiteSpace(q.Status), (e, sc, wr) => e.Status == q.Status)
- .WhereIF(string.IsNullOrWhiteSpace(q.Status) && q.StatusBucket == "pending",
- (e, sc, wr) => pendingStatuses.Contains(e.Status))
- // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:legacy 兼容 — q.Severity 传 LOW/MEDIUM/HIGH/CRITICAL 时
- // 通过 Normalize 映射为 FOLLOW/SERIOUS 再过滤。
- .WhereIF(!string.IsNullOrWhiteSpace(q.Severity),
- (e, sc, wr) => e.Severity == S8SeverityCode.Normalize(q.Severity))
- .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), (e, sc, wr) => e.SceneCode == q.SceneCode)
- .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), (e, sc, wr) => e.ModuleCode == q.ModuleCode)
- // S8-DASHBOARD-DATA-ALIGN-S1S7-1:看板明细表传 OnlyS1S7Modules=true 时与 KPI 口径对齐;
- // 异常列表页不传该参数,行为保持不变(仍可见 NULL module 的 legacy 行)。
- .WhereIF(q.OnlyS1S7Modules == true, (e, sc, wr) => S8ModuleCode.All.Contains(e.ModuleCode))
- .WhereIF(q.DeptId.HasValue, (e, sc, wr) => e.ResponsibleDeptId == q.DeptId!.Value || e.OccurrenceDeptId == q.DeptId!.Value)
- .WhereIF(q.TimeoutFlag.HasValue, (e, sc, wr) => e.TimeoutFlag == q.TimeoutFlag!.Value)
- .WhereIF(q.BeginTime.HasValue, (e, sc, wr) => e.CreatedAt >= q.BeginTime!.Value)
- .WhereIF(q.EndTime.HasValue, (e, sc, wr) => e.CreatedAt <= q.EndTime!.Value)
- .WhereIF(!string.IsNullOrWhiteSpace(q.ProcessNodeCode), (e, sc, wr) => e.ProcessNodeCode == q.ProcessNodeCode)
- .WhereIF(!string.IsNullOrWhiteSpace(q.RelatedObjectCode), (e, sc, wr) => e.RelatedObjectCode == q.RelatedObjectCode)
- .WhereIF(q.RecoveredStatus == "RECOVERED", (e, sc, wr) => e.RecoveredAt != null)
- .WhereIF(q.RecoveredStatus == "ACTIVE", (e, sc, wr) => e.RecoveredAt == null)
- .WhereIF(!string.IsNullOrWhiteSpace(q.RuleType), (e, sc, wr) => wr.RuleType == q.RuleType)
- .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
- (e, sc, wr) => e.Title.Contains(q.Keyword!) || e.ExceptionCode.Contains(q.Keyword!));
- var total = await query.CountAsync();
- var list = await query
- .OrderBy((e, sc, wr) => e.CreatedAt, OrderByType.Desc)
- .Select((e, sc, wr) => 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,
- ModuleCode = e.ModuleCode,
- ResponsibleDeptId = e.ResponsibleDeptId,
- OccurrenceDeptId = e.OccurrenceDeptId,
- AssigneeId = e.AssigneeId,
- SlaDeadline = e.SlaDeadline,
- TimeoutFlag = e.TimeoutFlag,
- CreatedAt = e.CreatedAt,
- ClosedAt = e.ClosedAt,
- ExceptionTypeCode = e.ExceptionTypeCode,
- RecoveredAt = e.RecoveredAt,
- SourceRuleCode = e.SourceRuleCode,
- SourceObjectType = e.SourceObjectType,
- SourceObjectId = e.SourceObjectId,
- DedupKey = e.DedupKey,
- LastDetectedAt = e.LastDetectedAt,
- RuleType = wr.RuleType
- })
- .ToPageListAsync(q.Page, q.PageSize);
- foreach (var r in list)
- {
- r.StatusLabel = S8Labels.StatusLabel(r.Status);
- r.SeverityLabel = S8Labels.SeverityLabel(r.Severity);
- r.ModuleName = string.IsNullOrWhiteSpace(r.ModuleCode) ? null : S8ModuleCode.Label(r.ModuleCode!);
- }
- await FillDisplayNamesAsync(list, q.FactoryId);
- return (total, list);
- }
- public async Task<object> GetFilterOptionsAsync(long tenantId, long factoryId)
- {
- var scenes = await _rep.Context.Queryable<AdoS8SceneConfig>()
- .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();
- // S8-EXCEPTION-MODULE-DISPLAY-1:业务展示主口径切到 module_code(S1-S7)。
- // scenes 保留兼容期,前端筛选已切到 modules。
- var modules = S8ModuleCode.All
- .Select(code => new { value = code, label = S8ModuleCode.Label(code) })
- .ToList();
- return new
- {
- statuses = S8Labels.StatusOptions(),
- severities = S8Labels.SeverityOptions(),
- scenes,
- modules,
- departments
- };
- }
- public async Task<AdoS8ExceptionDetailDto?> GetDetailAsync(long id, long tenantId, long factoryId)
- {
- var rows = await _rep.Context.Queryable<AdoS8Exception>()
- .LeftJoin<AdoS8SceneConfig>((e, sc) =>
- e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
- .LeftJoin<AdoS8WatchRule>((e, sc, wr) =>
- e.TenantId == wr.TenantId && e.FactoryId == wr.FactoryId && e.SourceRuleCode == wr.RuleCode)
- .Where((e, sc, wr) => e.Id == id && e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
- .Select((e, sc, wr) => 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,
- ModuleCode = e.ModuleCode,
- 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,
- ExceptionTypeCode = e.ExceptionTypeCode,
- RuleType = wr.RuleType,
- })
- .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);
- d.ModuleName = string.IsNullOrWhiteSpace(d.ModuleCode) ? null : S8ModuleCode.Label(d.ModuleCode!);
- await FillDisplayNamesAsync(new[] { d }, factoryId);
- return d;
- }
- // 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.Select(x => x.OccurrenceDeptId))
- .Where(x => x > 0)
- .Distinct()
- .ToList();
- var empIds = list
- .Select(x => x.AssigneeId ?? 0L)
- .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.ReporterId ?? 0L))
- .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.VerifierId ?? 0L))
- .Where(x => x > 0)
- .Distinct()
- .ToList();
- var deptMap = deptIds.Count == 0
- ? new Dictionary<long, string>()
- : (await _deptRep.AsQueryable()
- .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);
- // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位(797403760988229 ≠ 系统租户),
- // 走全局 multi-tenant filter 会全过滤为空 → 详情页处理人/检验人姓名显示数字 ID。
- // 与 S8MasterDataAdapter.GetEmployeesAsync / S8TaskFlowService.GetEmployeeSysUserIdAsync 同口径,
- // 局部 ClearFilter + employee.Id 主键集合作为安全边界,无跨租户泄漏。
- var empMap = empIds.Count == 0
- ? new Dictionary<long, string>()
- : (await _empRep.AsQueryable().ClearFilter()
- .Where(x => empIds.Contains(x.Id))
- .Select(x => new { x.Id, Name = x.Name ?? x.Employee })
- .ToListAsync())
- .ToDictionary(x => x.Id, x => x.Name);
- // S8-REPORTER-IDSPACE-FIX-1(P0-B-1):ReporterId 新协议 = EmployeeMaster.RecID(与 assignee/verifier 同 idspace)。
- // 水合优先级:EmployeeMaster.Name → fallback SysUser.RealName/Account(兼容旧数据 reporter=SysUser.Id)。
- // 旧注释 “ReporterId 现在记录 sysUser.Id” 已失效,由本批协议替代。
- 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)
- {
- 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)
- {
- // S8-REPORTER-IDSPACE-FIX-1:empMap(EmployeeMaster.RecID)优先,reporterUserMap(SysUser.Id)兜底兼容旧数据。
- detail.ReporterName = detail.ReporterId.HasValue
- ? (empMap.GetValueOrDefault(detail.ReporterId.Value) ?? reporterUserMap.GetValueOrDefault(detail.ReporterId.Value))
- : null;
- detail.VerifierName = detail.VerifierId.HasValue ? empMap.GetValueOrDefault(detail.VerifierId.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 守门。
- /// 实现纪律:
- /// - 仅 SET IsDeleted=true,不动业务字段(status/closed_at/source_rule_id 等)。
- /// - 前缀字面量硬编码,不接受外部 prefix;防止前缀注入。
- /// - 不联动 timeline / decision / evidence;业务读路径以父 IsDeleted 屏蔽。
- /// </summary>
- public async Task<int> 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;
- }
- }
|