S8ManualReportService.cs 10.0 KB

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