S8ExceptionService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. using Admin.NET.Plugin.AiDOP.Dto.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure;
  6. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  7. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  8. public class S8ExceptionService : ITransient
  9. {
  10. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  11. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  12. private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
  13. private readonly SqlSugarRepository<SysUser> _sysUserRep;
  14. public S8ExceptionService(
  15. SqlSugarRepository<AdoS8Exception> rep,
  16. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  17. SqlSugarRepository<AdoS0EmployeeMaster> empRep,
  18. SqlSugarRepository<SysUser> sysUserRep)
  19. {
  20. _rep = rep;
  21. _deptRep = deptRep;
  22. _empRep = empRep;
  23. _sysUserRep = sysUserRep;
  24. }
  25. public async Task<(int total, List<AdoS8ExceptionListItemDto> list)> GetPagedAsync(AdoS8ExceptionQueryDto q)
  26. {
  27. (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
  28. var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" };
  29. var query = _rep.Context.Queryable<AdoS8Exception>()
  30. .LeftJoin<AdoS8SceneConfig>((e, sc) =>
  31. e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
  32. .Where((e, sc) => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
  33. .WhereIF(!string.IsNullOrWhiteSpace(q.Status), (e, sc) => e.Status == q.Status)
  34. .WhereIF(string.IsNullOrWhiteSpace(q.Status) && q.StatusBucket == "pending",
  35. (e, sc) => pendingStatuses.Contains(e.Status))
  36. .WhereIF(!string.IsNullOrWhiteSpace(q.Severity), (e, sc) => e.Severity == q.Severity)
  37. .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), (e, sc) => e.SceneCode == q.SceneCode)
  38. .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), (e, sc) => e.ModuleCode == q.ModuleCode)
  39. .WhereIF(q.DeptId.HasValue, (e, sc) => e.ResponsibleDeptId == q.DeptId!.Value || e.OccurrenceDeptId == q.DeptId!.Value)
  40. .WhereIF(q.TimeoutFlag.HasValue, (e, sc) => e.TimeoutFlag == q.TimeoutFlag!.Value)
  41. .WhereIF(q.BeginTime.HasValue, (e, sc) => e.CreatedAt >= q.BeginTime!.Value)
  42. .WhereIF(q.EndTime.HasValue, (e, sc) => e.CreatedAt <= q.EndTime!.Value)
  43. .WhereIF(!string.IsNullOrWhiteSpace(q.ProcessNodeCode), (e, sc) => e.ProcessNodeCode == q.ProcessNodeCode)
  44. .WhereIF(!string.IsNullOrWhiteSpace(q.RelatedObjectCode), (e, sc) => e.RelatedObjectCode == q.RelatedObjectCode)
  45. .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
  46. (e, sc) => e.Title.Contains(q.Keyword!) || e.ExceptionCode.Contains(q.Keyword!));
  47. var total = await query.CountAsync();
  48. var list = await query
  49. .OrderBy((e, sc) => e.CreatedAt, OrderByType.Desc)
  50. .Select((e, sc) => new AdoS8ExceptionListItemDto
  51. {
  52. Id = e.Id,
  53. FactoryId = e.FactoryId,
  54. ExceptionCode = e.ExceptionCode,
  55. Title = e.Title,
  56. Status = e.Status,
  57. Severity = e.Severity,
  58. PriorityScore = e.PriorityScore,
  59. PriorityLevel = e.PriorityLevel,
  60. SceneCode = e.SceneCode,
  61. SceneName = sc.SceneName,
  62. ResponsibleDeptId = e.ResponsibleDeptId,
  63. AssigneeId = e.AssigneeId,
  64. SlaDeadline = e.SlaDeadline,
  65. TimeoutFlag = e.TimeoutFlag,
  66. CreatedAt = e.CreatedAt,
  67. ClosedAt = e.ClosedAt
  68. })
  69. .ToPageListAsync(q.Page, q.PageSize);
  70. foreach (var r in list)
  71. {
  72. r.StatusLabel = S8Labels.StatusLabel(r.Status);
  73. r.SeverityLabel = S8Labels.SeverityLabel(r.Severity);
  74. }
  75. await FillDisplayNamesAsync(list);
  76. return (total, list);
  77. }
  78. public async Task<object> GetFilterOptionsAsync(long tenantId, long factoryId)
  79. {
  80. var scenes = await _rep.Context.Queryable<AdoS8SceneConfig>()
  81. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled)
  82. .OrderBy(x => x.SortNo)
  83. .Select(x => new { value = x.SceneCode, label = x.SceneName })
  84. .ToListAsync();
  85. var departments = await _deptRep.AsQueryable()
  86. .Where(x => x.FactoryRefId == factoryId)
  87. .OrderBy(x => x.Department)
  88. .Take(500)
  89. .Select(x => new { value = x.Id, label = x.Descr ?? x.Department })
  90. .ToListAsync();
  91. return new
  92. {
  93. statuses = S8Labels.StatusOptions(),
  94. severities = S8Labels.SeverityOptions(),
  95. scenes,
  96. departments
  97. };
  98. }
  99. public async Task<AdoS8ExceptionDetailDto?> GetDetailAsync(long id, long tenantId, long factoryId)
  100. {
  101. var rows = await _rep.Context.Queryable<AdoS8Exception>()
  102. .LeftJoin<AdoS8SceneConfig>((e, sc) =>
  103. e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
  104. .Where((e, sc) => e.Id == id && e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
  105. .Select((e, sc) => new AdoS8ExceptionDetailDto
  106. {
  107. Id = e.Id,
  108. FactoryId = e.FactoryId,
  109. ExceptionCode = e.ExceptionCode,
  110. Title = e.Title,
  111. Description = e.Description,
  112. Status = e.Status,
  113. Severity = e.Severity,
  114. PriorityScore = e.PriorityScore,
  115. PriorityLevel = e.PriorityLevel,
  116. SceneCode = e.SceneCode,
  117. SceneName = sc.SceneName,
  118. SourceType = e.SourceType,
  119. OccurrenceDeptId = e.OccurrenceDeptId,
  120. ResponsibleDeptId = e.ResponsibleDeptId,
  121. ResponsibleGroupId = e.ResponsibleGroupId,
  122. AssigneeId = e.AssigneeId,
  123. ReporterId = e.ReporterId,
  124. SlaDeadline = e.SlaDeadline,
  125. TimeoutFlag = e.TimeoutFlag,
  126. CreatedAt = e.CreatedAt,
  127. ClosedAt = e.ClosedAt,
  128. AssignedAt = e.AssignedAt,
  129. UpdatedAt = e.UpdatedAt,
  130. ActiveFlowInstanceId = e.ActiveFlowInstanceId,
  131. ActiveFlowBizType = e.ActiveFlowBizType,
  132. VerifierId = e.VerifierId,
  133. VerificationAssignedAt = e.VerificationAssignedAt,
  134. VerifiedAt = e.VerifiedAt,
  135. VerificationResult = e.VerificationResult,
  136. VerificationRemark = e.VerificationRemark,
  137. SourceRuleId = e.SourceRuleId,
  138. RelatedObjectCode = e.RelatedObjectCode,
  139. DedupKey = e.DedupKey,
  140. LastDetectedAt = e.LastDetectedAt,
  141. RecoveredAt = e.RecoveredAt,
  142. SourceRuleCode = e.SourceRuleCode,
  143. SourceObjectType = e.SourceObjectType,
  144. SourceObjectId = e.SourceObjectId,
  145. })
  146. .Take(1)
  147. .ToListAsync();
  148. if (rows.Count == 0) return null;
  149. var d = rows[0];
  150. d.StatusLabel = S8Labels.StatusLabel(d.Status);
  151. d.SeverityLabel = S8Labels.SeverityLabel(d.Severity);
  152. await FillDisplayNamesAsync(new[] { d });
  153. return d;
  154. }
  155. private async Task FillDisplayNamesAsync(IEnumerable<AdoS8ExceptionListItemDto> rows)
  156. {
  157. var list = rows.ToList();
  158. if (list.Count == 0) return;
  159. var deptIds = list
  160. .Select(x => x.ResponsibleDeptId)
  161. .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.OccurrenceDeptId))
  162. .Where(x => x > 0)
  163. .Distinct()
  164. .ToList();
  165. var empIds = list
  166. .Select(x => x.AssigneeId ?? 0L)
  167. .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.ReporterId ?? 0L))
  168. .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.VerifierId ?? 0L))
  169. .Where(x => x > 0)
  170. .Distinct()
  171. .ToList();
  172. var deptMap = deptIds.Count == 0
  173. ? new Dictionary<long, string>()
  174. : (await _deptRep.AsQueryable()
  175. .Where(x => deptIds.Contains(x.Id))
  176. .Select(x => new { x.Id, Name = x.Descr ?? x.Department })
  177. .ToListAsync())
  178. .ToDictionary(x => x.Id, x => x.Name);
  179. var empMap = empIds.Count == 0
  180. ? new Dictionary<long, string>()
  181. : (await _empRep.AsQueryable()
  182. .Where(x => empIds.Contains(x.Id))
  183. .Select(x => new { x.Id, Name = x.Name ?? x.Employee })
  184. .ToListAsync())
  185. .ToDictionary(x => x.Id, x => x.Name);
  186. // ReporterId 现在记录 sysUser.Id(OBS-S8-REPORT-REPORTER-NULL-001 修复后由服务端 JWT 兜底),
  187. // 与 employee.Id 不在同一命名空间,需要单独查 SysUser 取 RealName/Account;
  188. // 同时保留对历史 reporter=employee.Id 数据的兼容(empMap fallback)。
  189. var reporterUserIds = list.OfType<AdoS8ExceptionDetailDto>()
  190. .Select(x => x.ReporterId ?? 0L)
  191. .Where(x => x > 0)
  192. .Distinct()
  193. .ToList();
  194. var reporterUserMap = reporterUserIds.Count == 0
  195. ? new Dictionary<long, string>()
  196. : (await _sysUserRep.AsQueryable().ClearFilter()
  197. .Where(u => reporterUserIds.Contains(u.Id))
  198. .Select(u => new { u.Id, u.RealName, u.Account })
  199. .ToListAsync())
  200. .ToDictionary(
  201. u => u.Id,
  202. u => !string.IsNullOrWhiteSpace(u.RealName) ? u.RealName : u.Account);
  203. foreach (var row in list)
  204. {
  205. if (row is AdoS8ExceptionDetailDto detail)
  206. {
  207. detail.ResponsibleDeptName = deptMap.GetValueOrDefault(detail.ResponsibleDeptId);
  208. detail.OccurrenceDeptName = deptMap.GetValueOrDefault(detail.OccurrenceDeptId);
  209. detail.AssigneeName = detail.AssigneeId.HasValue ? empMap.GetValueOrDefault(detail.AssigneeId.Value) : null;
  210. detail.ReporterName = detail.ReporterId.HasValue
  211. ? (reporterUserMap.GetValueOrDefault(detail.ReporterId.Value) ?? empMap.GetValueOrDefault(detail.ReporterId.Value))
  212. : null;
  213. detail.VerifierName = detail.VerifierId.HasValue ? empMap.GetValueOrDefault(detail.VerifierId.Value) : null;
  214. }
  215. else
  216. {
  217. row.ResponsibleDeptName = deptMap.GetValueOrDefault(row.ResponsibleDeptId);
  218. row.AssigneeName = row.AssigneeId.HasValue ? empMap.GetValueOrDefault(row.AssigneeId.Value) : null;
  219. }
  220. }
  221. }
  222. /// <summary>
  223. /// S2-EW:debug-only 软删 RelatedObjectCode 以 "TEST_G09_" 开头的异常。
  224. /// 调用方仅限 <see cref="Controllers.S8.AdoS8WatchDebugController"/>,由其 _debugEndpointEnabled 守门。
  225. /// 实现纪律:
  226. /// - 仅 SET IsDeleted=true,不动业务字段(status/closed_at/source_rule_id 等)。
  227. /// - 前缀字面量硬编码,不接受外部 prefix;防止前缀注入。
  228. /// - 不联动 timeline / decision / evidence;业务读路径以父 IsDeleted 屏蔽。
  229. /// </summary>
  230. public async Task<int> SoftDeleteTestPrefixAsync(long tenantId, long factoryId)
  231. {
  232. const string prefix = "TEST_G09_";
  233. var affected = await _rep.AsUpdateable()
  234. .SetColumns(e => new AdoS8Exception { IsDeleted = true })
  235. .Where(e => e.TenantId == tenantId
  236. && e.FactoryId == factoryId
  237. && !e.IsDeleted
  238. && e.RelatedObjectCode != null
  239. && e.RelatedObjectCode.StartsWith(prefix))
  240. .ExecuteCommandAsync();
  241. System.Diagnostics.Trace.TraceWarning(
  242. $"[S2-EW] soft-deleted {affected} TEST_G09_ exceptions, tenant={tenantId} factory={factoryId}");
  243. return affected;
  244. }
  245. }