S8ManualReportService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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.S8;
  6. using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  7. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  8. public class S8ManualReportService : ITransient
  9. {
  10. // 合法严重度白名单(与 GetFormOptionsAsync.severities 同源);前端/后端默认值 MEDIUM。
  11. private static readonly HashSet<string> AllowedSeverities = new(StringComparer.Ordinal)
  12. {
  13. "CRITICAL", "HIGH", "MEDIUM", "LOW"
  14. };
  15. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  16. private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
  17. private readonly SqlSugarRepository<AdoS8Evidence> _evidenceRep;
  18. private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
  19. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  20. private readonly SqlSugarRepository<AdoS0LineMaster> _lineRep;
  21. private readonly UserManager _userManager;
  22. public S8ManualReportService(
  23. SqlSugarRepository<AdoS8Exception> rep,
  24. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  25. SqlSugarRepository<AdoS8Evidence> evidenceRep,
  26. SqlSugarRepository<AdoS8SceneConfig> sceneRep,
  27. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  28. SqlSugarRepository<AdoS0LineMaster> lineRep,
  29. UserManager userManager)
  30. {
  31. _rep = rep;
  32. _timelineRep = timelineRep;
  33. _evidenceRep = evidenceRep;
  34. _sceneRep = sceneRep;
  35. _deptRep = deptRep;
  36. _lineRep = lineRep;
  37. _userManager = userManager;
  38. }
  39. public async Task<object> GetFormOptionsAsync(long tenantId, long factoryId)
  40. {
  41. var scenes = await _sceneRep.AsQueryable()
  42. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled)
  43. .OrderBy(x => x.SortNo)
  44. .Select(x => new { value = x.SceneCode, label = x.SceneName })
  45. .ToListAsync();
  46. var departments = await _deptRep.AsQueryable()
  47. .Where(x => x.FactoryRefId == factoryId)
  48. .OrderBy(x => x.Department)
  49. .Take(500)
  50. .Select(x => new { value = x.Id, label = x.Descr ?? x.Department })
  51. .ToListAsync();
  52. var lines = await _lineRep.AsQueryable()
  53. .Where(x => x.FactoryRefId == factoryId)
  54. .OrderBy(x => x.Line)
  55. .Take(500)
  56. .Select(x => new { value = x.Id, label = x.Describe ?? x.Line })
  57. .ToListAsync();
  58. return new
  59. {
  60. scenes,
  61. severities = new[]
  62. {
  63. new { value = "CRITICAL", label = "紧急" },
  64. new { value = "HIGH", label = "高" },
  65. new { value = "MEDIUM", label = "中" },
  66. new { value = "LOW", label = "低" }
  67. },
  68. departments,
  69. lines,
  70. materials = Array.Empty<object>()
  71. };
  72. }
  73. public async Task<AdoS8ManualReportResultDto> CreateAsync(AdoS8ManualReportCreateDto dto)
  74. {
  75. if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
  76. if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
  77. // 严重度白名单校验:空值兜底为 MEDIUM;非法值直接拒绝,避免 P3 等错位写入。
  78. var severity = string.IsNullOrWhiteSpace(dto.Severity) ? "MEDIUM" : dto.Severity.Trim();
  79. if (!AllowedSeverities.Contains(severity))
  80. throw new S8BizException($"严重度 {severity} 非法,仅允许 CRITICAL/HIGH/MEDIUM/LOW");
  81. // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
  82. var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
  83. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  84. var entity = new AdoS8Exception
  85. {
  86. TenantId = dto.TenantId,
  87. FactoryId = dto.FactoryId,
  88. ExceptionCode = code,
  89. Title = dto.Title.Trim(),
  90. Description = dto.Description,
  91. SceneCode = dto.SceneCode.Trim(),
  92. SourceType = "MANUAL",
  93. Status = "NEW",
  94. Severity = severity,
  95. PriorityScore = 0,
  96. PriorityLevel = "P3",
  97. OccurrenceDeptId = dto.OccurrenceDeptId,
  98. ResponsibleDeptId = dto.ResponsibleDeptId,
  99. ReporterId = currentUserId,
  100. CreatedAt = DateTime.Now,
  101. IsDeleted = false
  102. };
  103. await _rep.AsTenant().UseTranAsync(async () =>
  104. {
  105. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  106. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  107. {
  108. ExceptionId = entity.Id,
  109. ActionCode = "CREATE",
  110. ActionLabel = "创建",
  111. FromStatus = null,
  112. ToStatus = "NEW",
  113. OperatorId = currentUserId,
  114. ActionRemark = "主动提报",
  115. CreatedAt = DateTime.Now
  116. });
  117. }, ex => throw ex);
  118. return new AdoS8ManualReportResultDto
  119. {
  120. ExceptionId = entity.Id,
  121. ExceptionCode = entity.ExceptionCode,
  122. TaskId = entity.Id
  123. };
  124. }
  125. /// <summary>
  126. /// G01-06:自动建单分支(非第二套创建主链)。
  127. /// 这是本服务内的自动监控建单路径,与 <see cref="CreateAsync"/> 并列,
  128. /// 复用同一仓储(_rep / _timelineRep)、同一事务边界、同一 ExceptionCode 生成规则、
  129. /// 同一时间线主链(ActionCode="CREATE"、ToStatus="NEW");仅差异点:
  130. /// - SourceType 标识为自动监控来源
  131. /// - 填入 SourceRuleId / SourceDataSourceId / SourcePayload / RelatedObjectCode 追溯
  132. /// - ExceptionTypeCode 固定 EQUIP_FAULT(G-01 首版唯一映射)
  133. /// - SceneCode 固定 S2S6_PRODUCTION(G-01 首版唯一场景)
  134. /// 不做补偿、重试、对账;失败由调用方接住。
  135. /// </summary>
  136. public async Task<AdoS8Exception> CreateFromWatchAsync(S8WatchHitResult hit)
  137. {
  138. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  139. throw new S8BizException("自动建单缺失追溯键");
  140. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  141. var title = $"[自动] 设备 {hit.RelatedObjectCode} {hit.TriggerCondition} {hit.ThresholdValue}(当前 {hit.CurrentValue})";
  142. var entity = new AdoS8Exception
  143. {
  144. // 租户/工厂:与 S8WatchSchedulerService.RunOnceAsync 当前固定上下文一致。
  145. TenantId = 1,
  146. FactoryId = 1,
  147. ExceptionCode = code,
  148. Title = title,
  149. Description = null,
  150. SceneCode = S8SceneCode.S2S6Production,
  151. // 首版自动监控建单来源标识(字符串值,先不抽常量类)。
  152. SourceType = "AUTO_WATCH",
  153. Status = "NEW",
  154. Severity = string.IsNullOrWhiteSpace(hit.Severity) ? "MEDIUM" : hit.Severity,
  155. PriorityScore = 0,
  156. PriorityLevel = "P3",
  157. // 首版兜底口径:Hit 未提供部门时置 0 仅为保证“能建成标准异常单并进入主链”,
  158. // 不是最终业务部门语义;后续需由上游查询结果提供,或在专项任务中补口径。
  159. OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
  160. ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
  161. ReporterId = null,
  162. CreatedAt = DateTime.Now,
  163. IsDeleted = false,
  164. // G-01 首版唯一异常类型映射(seed 已确认 EQUIP_FAULT 属 S2S6_PRODUCTION 场景)。
  165. ExceptionTypeCode = "EQUIP_FAULT",
  166. // ModuleCode:S2S6_PRODUCTION 场景对应 S2+S6 两个模块(见 S8ModuleCode.SceneOf),
  167. // 无稳定“scene → 单一 module”映射;首版置空,不靠经验写死。
  168. ModuleCode = null,
  169. ProcessNodeCode = null,
  170. // 追溯三件套(自动建单必填口径)。
  171. SourceRuleId = hit.SourceRuleId,
  172. SourceDataSourceId = hit.DataSourceId,
  173. SourcePayload = hit.SourcePayload,
  174. RelatedObjectCode = hit.RelatedObjectCode
  175. };
  176. await _rep.AsTenant().UseTranAsync(async () =>
  177. {
  178. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  179. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  180. {
  181. ExceptionId = entity.Id,
  182. ActionCode = "CREATE",
  183. ActionLabel = "创建",
  184. FromStatus = null,
  185. ToStatus = "NEW",
  186. OperatorId = null,
  187. ActionRemark = "自动建单",
  188. CreatedAt = DateTime.Now
  189. });
  190. }, ex => throw ex);
  191. return entity;
  192. }
  193. /// <summary>
  194. /// R2 自动建单分支(TIMEOUT 等新 evaluator 走此路径)。
  195. /// 与 <see cref="CreateFromWatchAsync"/> 并列:复用同一仓储 / 事务 / 时间线 ActionCode;
  196. /// 差异点:消费 <see cref="S8RuleHit"/> 一份命中模型,把 R2 新列(DedupKey / LastDetectedAt /
  197. /// SourceRuleCode / SourceObjectType / SourceObjectId)落齐;ExceptionTypeCode 由 hit 自带,
  198. /// 不再硬编码 EQUIP_FAULT。RecoveredAt 本轮不写。
  199. /// 调用方负责前置检查 ExceptionTypeCode 是否在 baseline;本方法不再二次校验。
  200. /// </summary>
  201. public async Task<AdoS8Exception> CreateFromHitAsync(S8RuleHit hit)
  202. {
  203. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  204. throw new S8BizException("自动建单缺失追溯键");
  205. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  206. var entity = new AdoS8Exception
  207. {
  208. // 与 CreateFromWatchAsync 当前固定上下文一致;R2 不变更租户/工厂上下文语义。
  209. TenantId = 1,
  210. FactoryId = 1,
  211. ExceptionCode = code,
  212. Title = string.IsNullOrWhiteSpace(hit.Title)
  213. ? $"[自动] {hit.SourceObjectType} {hit.SourceObjectId}"
  214. : hit.Title,
  215. Description = null,
  216. SceneCode = string.IsNullOrWhiteSpace(hit.SceneCode) ? S8SceneCode.S2S6Production : hit.SceneCode,
  217. SourceType = "AUTO_WATCH",
  218. Status = "NEW",
  219. Severity = string.IsNullOrWhiteSpace(hit.Severity) ? "MEDIUM" : hit.Severity,
  220. PriorityScore = 0,
  221. PriorityLevel = "P3",
  222. OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
  223. ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
  224. ReporterId = null,
  225. CreatedAt = DateTime.Now,
  226. IsDeleted = false,
  227. ExceptionTypeCode = hit.ExceptionTypeCode,
  228. ModuleCode = null,
  229. ProcessNodeCode = null,
  230. SourceRuleId = hit.SourceRuleId,
  231. SourceDataSourceId = hit.DataSourceId == 0 ? null : hit.DataSourceId,
  232. SourcePayload = hit.SourcePayload,
  233. RelatedObjectCode = hit.RelatedObjectCode,
  234. // R2 新列回填
  235. DedupKey = hit.DedupKey,
  236. LastDetectedAt = hit.DetectedAt,
  237. RecoveredAt = null,
  238. SourceRuleCode = hit.SourceRuleCode,
  239. SourceObjectType = hit.SourceObjectType,
  240. SourceObjectId = hit.SourceObjectId
  241. };
  242. await _rep.AsTenant().UseTranAsync(async () =>
  243. {
  244. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  245. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  246. {
  247. ExceptionId = entity.Id,
  248. ActionCode = "CREATE",
  249. ActionLabel = "创建",
  250. FromStatus = null,
  251. ToStatus = "NEW",
  252. OperatorId = null,
  253. ActionRemark = "自动建单(R2)",
  254. CreatedAt = DateTime.Now
  255. });
  256. }, ex => throw ex);
  257. return entity;
  258. }
  259. public async Task<AdoS8Exception?> GetAsync(long id) =>
  260. await _rep.GetByIdAsync(id);
  261. public async Task<AdoS8Evidence> AddAttachmentAsync(long id, AdoS8AttachmentCreateDto dto)
  262. {
  263. var entity = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
  264. ?? throw new S8BizException("异常不存在");
  265. if (string.IsNullOrWhiteSpace(dto.FileName) || string.IsNullOrWhiteSpace(dto.FileUrl))
  266. throw new S8BizException("附件名称和地址必填");
  267. var evidence = new AdoS8Evidence
  268. {
  269. ExceptionId = id,
  270. EvidenceType = string.IsNullOrWhiteSpace(dto.EvidenceType) ? "file" : dto.EvidenceType,
  271. FileName = dto.FileName.Trim(),
  272. FileUrl = dto.FileUrl.Trim(),
  273. SourceSystem = dto.SourceSystem,
  274. UploadedBy = dto.UploadedBy,
  275. UploadedAt = DateTime.Now,
  276. IsDeleted = false
  277. };
  278. await _evidenceRep.InsertAsync(evidence);
  279. return evidence;
  280. }
  281. }