S8ManualReportService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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. using Admin.NET.Plugin.ApprovalFlow.Service;
  8. using Microsoft.Extensions.Logging;
  9. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  10. public class S8ManualReportService : ITransient
  11. {
  12. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举只保留 FOLLOW/SERIOUS。
  13. // 旧值 LOW/MEDIUM/HIGH/CRITICAL 仍可作为兼容输入(接收后由 S8SeverityCode.Normalize 归一)。
  14. private static readonly HashSet<string> AllowedSeverities = new(StringComparer.OrdinalIgnoreCase)
  15. {
  16. "FOLLOW", "SERIOUS",
  17. "LOW", "MEDIUM", "HIGH", "CRITICAL", // legacy compat
  18. };
  19. // S8-PROCESS-NODE-S1S7-ALIGN-1:process_node_code 当前阶段对齐 S1-S7 订单主流程。
  20. // 优先 module_code(已是 S1-S7),其次按 scene_code 反推;都无法识别则 null。
  21. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:保留函数挂起待用——本阶段所有建单点不再调用,
  22. // 由 module_code 承担 S1-S7 主流程归属;未来引入更细流程节点(如 S2.PLAN / S6.WO_RELEASE)时恢复使用。
  23. private static string? ResolveProcessNodeCode(string? sceneCode, string? moduleCode)
  24. {
  25. if (!string.IsNullOrWhiteSpace(moduleCode) && S8ModuleCode.All.Contains(moduleCode))
  26. return moduleCode;
  27. var fromScene = S8ModuleCode.FromScene(sceneCode);
  28. if (!string.IsNullOrWhiteSpace(fromScene) && S8ModuleCode.All.Contains(fromScene))
  29. return fromScene;
  30. return null;
  31. }
  32. /// <summary>
  33. /// S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:建单 module_code 派生统一入口。
  34. /// 优先级:显式 module → hit module → 单模块 sceneCode(S1-S7)→ exception_type.scene_code。
  35. /// 严格 S1-S7 校验,不接受 legacy 复合 scene;最终无法确定时返回 null(caller 决定记日志或拒绝)。
  36. /// </summary>
  37. private async Task<string?> ResolveModuleCodeAsync(
  38. string? explicitModuleCode,
  39. string? hitModuleCode,
  40. string? sceneCode,
  41. string? exceptionTypeCode)
  42. {
  43. var byExplicit = S8ModuleCode.Normalize(explicitModuleCode);
  44. if (byExplicit != null) return byExplicit;
  45. var byHit = S8ModuleCode.Normalize(hitModuleCode);
  46. if (byHit != null) return byHit;
  47. var byScene = S8ModuleCode.FromCanonicalScene(sceneCode);
  48. if (byScene != null) return byScene;
  49. if (!string.IsNullOrWhiteSpace(exceptionTypeCode))
  50. {
  51. var typeScene = await _typeRep.AsQueryable().ClearFilter()
  52. .Where(t => t.TypeCode == exceptionTypeCode && t.Enabled)
  53. .OrderByDescending(t => t.FactoryId)
  54. .Select(t => t.SceneCode)
  55. .FirstAsync();
  56. var byType = S8ModuleCode.FromCanonicalScene(typeScene);
  57. if (byType != null) return byType;
  58. }
  59. return null;
  60. }
  61. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  62. private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
  63. private readonly SqlSugarRepository<AdoS8Evidence> _evidenceRep;
  64. private readonly SqlSugarRepository<AdoS8SceneConfig> _sceneRep;
  65. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  66. private readonly SqlSugarRepository<AdoS0LineMaster> _lineRep;
  67. private readonly SqlSugarRepository<AdoS8ExceptionType> _typeRep;
  68. private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
  69. private readonly UserManager _userManager;
  70. private readonly FlowEngineService _flowEngine;
  71. private readonly ILogger<S8ManualReportService> _logger;
  72. public S8ManualReportService(
  73. SqlSugarRepository<AdoS8Exception> rep,
  74. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  75. SqlSugarRepository<AdoS8Evidence> evidenceRep,
  76. SqlSugarRepository<AdoS8SceneConfig> sceneRep,
  77. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  78. SqlSugarRepository<AdoS0LineMaster> lineRep,
  79. SqlSugarRepository<AdoS8ExceptionType> typeRep,
  80. SqlSugarRepository<AdoS0EmployeeMaster> empRep,
  81. UserManager userManager,
  82. FlowEngineService flowEngine,
  83. ILogger<S8ManualReportService> logger)
  84. {
  85. _rep = rep;
  86. _timelineRep = timelineRep;
  87. _evidenceRep = evidenceRep;
  88. _sceneRep = sceneRep;
  89. _deptRep = deptRep;
  90. _lineRep = lineRep;
  91. _typeRep = typeRep;
  92. _empRep = empRep;
  93. _userManager = userManager;
  94. _flowEngine = flowEngine;
  95. _logger = logger;
  96. }
  97. // S8-REPORTER-IDSPACE-FIX-1(P0-B-1):把当前登录 SysUser.Id 反查 EmployeeMaster.RecID。
  98. // 用于 reporter_id idspace 与 assignee_id / verifier_id 对齐。
  99. // 协议:sys_user_id + factory_ref_id 双键命中;EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位 →
  100. // ClearFilter,由 factoryRefId 做硬边界(与 S8MasterDataAdapter / S8TaskFlowService 同口径)。
  101. // 未绑定时返回 null,调用方负责诊断日志。
  102. private async Task<long?> ResolveCurrentEmployeeIdAsync(long factoryId, long sysUserId)
  103. {
  104. if (sysUserId <= 0 || factoryId <= 0) return null;
  105. var emp = await _empRep.AsQueryable().ClearFilter()
  106. .Where(x => x.SysUserId == sysUserId && x.FactoryRefId == factoryId && x.IsActive)
  107. .Select(x => new { x.Id })
  108. .FirstAsync();
  109. return emp?.Id;
  110. }
  111. /// <summary>
  112. /// 主动提报推断 ExceptionTypeCode:场景下取启用且 SortNo 最小的一条。
  113. /// baseline 异常类型当前 tenant_id=0/factory_id=0(全局基线),所以匹配条件为
  114. /// (tenantId 命中 OR 0) AND (factoryId 命中 OR 0)。ClearFilter 兜底全局多租户过滤器。
  115. /// 找不到返回 null(保持兼容)。
  116. /// </summary>
  117. private async Task<string?> InferExceptionTypeCodeAsync(long tenantId, long factoryId, string sceneCode)
  118. {
  119. if (string.IsNullOrWhiteSpace(sceneCode)) return null;
  120. return await _typeRep.AsQueryable().ClearFilter()
  121. .Where(x => (x.TenantId == tenantId || x.TenantId == 0)
  122. && (x.FactoryId == factoryId || x.FactoryId == 0)
  123. && x.SceneCode == sceneCode && x.Enabled)
  124. .OrderBy(x => x.SortNo)
  125. .Select(x => x.TypeCode)
  126. .FirstAsync();
  127. }
  128. /// <summary>
  129. /// TB001 异常提报审批流:自动监控 + 主动提报后软触发,失败仅 warn 日志,不阻断建单。
  130. /// </summary>
  131. private async Task TryStartIntakeFlowAsync(AdoS8Exception entity)
  132. {
  133. try
  134. {
  135. await _flowEngine.StartFlow(new StartFlowInput
  136. {
  137. BizType = "EXCEPTION_REPORT",
  138. BizId = entity.Id,
  139. BizNo = entity.ExceptionCode,
  140. Title = $"异常提报 - {entity.ExceptionCode}",
  141. Comment = entity.SourceType == "AUTO_WATCH" ? "自动监控触发" : "主动提报触发",
  142. BizData = new Dictionary<string, object>
  143. {
  144. ["sceneCode"] = entity.SceneCode ?? "",
  145. ["exceptionTypeCode"] = entity.ExceptionTypeCode ?? "",
  146. ["sourceType"] = entity.SourceType ?? ""
  147. }
  148. });
  149. }
  150. catch (Exception ex)
  151. {
  152. _logger.LogWarning(ex, "TB001 异常提报审批流触发失败 ExceptionId={Id} ExceptionCode={Code}", entity.Id, entity.ExceptionCode);
  153. }
  154. }
  155. public async Task<object> GetFormOptionsAsync(long tenantId, long factoryId)
  156. {
  157. var scenes = await _sceneRep.AsQueryable()
  158. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled)
  159. .OrderBy(x => x.SortNo)
  160. .Select(x => new { value = x.SceneCode, label = x.SceneName })
  161. .ToListAsync();
  162. // ClearFilter:DepartmentMaster.tenant_id 属 S0 域租户,不与登录 token TenantId 一致;
  163. // 用 factory_ref_id 做硬边界,安全等价。同 BUG-S8-EMPLOYEES-TENANT-FILTER 协议。
  164. var departments = await _deptRep.AsQueryable().ClearFilter()
  165. .Where(x => x.FactoryRefId == factoryId)
  166. .OrderBy(x => x.Department)
  167. .Take(500)
  168. .Select(x => new { value = x.Id, label = x.Descr ?? x.Department })
  169. .ToListAsync();
  170. var lines = await _lineRep.AsQueryable().ClearFilter()
  171. .Where(x => x.FactoryRefId == factoryId)
  172. .OrderBy(x => x.Line)
  173. .Take(500)
  174. .Select(x => new { value = x.Id, label = x.Describe ?? x.Line })
  175. .ToListAsync();
  176. return new
  177. {
  178. scenes,
  179. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举 FOLLOW/SERIOUS 两档。
  180. severities = S8SeverityCode.Options(),
  181. departments,
  182. lines,
  183. materials = Array.Empty<object>()
  184. };
  185. }
  186. public async Task<AdoS8ManualReportResultDto> CreateAsync(AdoS8ManualReportCreateDto dto)
  187. {
  188. if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
  189. if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
  190. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:白名单接受新值(FOLLOW/SERIOUS)+ 旧值兼容;
  191. // 通过 Normalize 写入 DB 一律为 FOLLOW/SERIOUS。
  192. var rawSeverity = string.IsNullOrWhiteSpace(dto.Severity) ? S8SeverityCode.Follow : dto.Severity.Trim();
  193. if (!AllowedSeverities.Contains(rawSeverity))
  194. throw new S8BizException($"严重度 {rawSeverity} 非法,仅允许 FOLLOW/SERIOUS");
  195. var severity = S8SeverityCode.Normalize(rawSeverity);
  196. // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
  197. // currentUserId 仍是 SysUser.Id,仅用作 timeline.OperatorId(与 S8TaskFlowService 同口径);
  198. // S8-REPORTER-IDSPACE-FIX-1:ReporterId 写入改用 EmployeeMaster.RecID(与 assignee/verifier 同 idspace)。
  199. var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
  200. long? currentEmployeeId = null;
  201. if (currentUserId.HasValue)
  202. {
  203. currentEmployeeId = await ResolveCurrentEmployeeIdAsync(dto.FactoryId, currentUserId.Value);
  204. if (currentEmployeeId == null)
  205. {
  206. _logger.LogWarning(
  207. "manual_report_reporter_unbound sysUserId={SysUserId} factoryId={FactoryId} title={Title}",
  208. currentUserId.Value, dto.FactoryId, dto.Title);
  209. }
  210. }
  211. // 主动提报无前端 type 字段,按场景兜底推断;保证不进"未分类"桶。
  212. var inferredType = await InferExceptionTypeCodeAsync(dto.TenantId, dto.FactoryId, dto.SceneCode.Trim());
  213. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:手工提报 module_code 严格按 S1-S7 派生;
  214. // dto 暂不携带显式 module_code,按 scene → exception_type.scene 链路降级。
  215. var resolvedModule = await ResolveModuleCodeAsync(
  216. explicitModuleCode: null,
  217. hitModuleCode: null,
  218. sceneCode: dto.SceneCode.Trim(),
  219. exceptionTypeCode: inferredType);
  220. if (resolvedModule == null)
  221. {
  222. _logger.LogWarning(
  223. "manual_report_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} title={Title}",
  224. dto.SceneCode, inferredType, dto.Title);
  225. }
  226. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  227. var entity = new AdoS8Exception
  228. {
  229. TenantId = dto.TenantId,
  230. FactoryId = dto.FactoryId,
  231. ExceptionCode = code,
  232. Title = dto.Title.Trim(),
  233. Description = dto.Description,
  234. SceneCode = dto.SceneCode.Trim(),
  235. SourceType = "MANUAL",
  236. Status = "NEW",
  237. Severity = severity,
  238. PriorityScore = 0,
  239. PriorityLevel = "P3",
  240. OccurrenceDeptId = dto.OccurrenceDeptId,
  241. ResponsibleDeptId = dto.ResponsibleDeptId,
  242. // S8-REPORTER-IDSPACE-FIX-1:reporter_id idspace = EmployeeMaster.RecID(与 assignee/verifier 同)。
  243. ReporterId = currentEmployeeId,
  244. ExceptionTypeCode = inferredType,
  245. ModuleCode = resolvedModule,
  246. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  247. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  248. ProcessNodeCode = null,
  249. CreatedAt = DateTime.Now,
  250. IsDeleted = false
  251. };
  252. await _rep.AsTenant().UseTranAsync(async () =>
  253. {
  254. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  255. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  256. {
  257. ExceptionId = entity.Id,
  258. ActionCode = "CREATE",
  259. ActionLabel = "创建",
  260. FromStatus = null,
  261. ToStatus = "NEW",
  262. OperatorId = currentUserId,
  263. ActionRemark = "主动提报",
  264. CreatedAt = DateTime.Now
  265. });
  266. }, ex => throw ex);
  267. await TryStartIntakeFlowAsync(entity);
  268. return new AdoS8ManualReportResultDto
  269. {
  270. ExceptionId = entity.Id,
  271. ExceptionCode = entity.ExceptionCode,
  272. TaskId = entity.Id
  273. };
  274. }
  275. /// <summary>
  276. /// G01-06:自动建单分支(非第二套创建主链)。
  277. /// 这是本服务内的自动监控建单路径,与 <see cref="CreateAsync"/> 并列,
  278. /// 复用同一仓储(_rep / _timelineRep)、同一事务边界、同一 ExceptionCode 生成规则、
  279. /// 同一时间线主链(ActionCode="CREATE"、ToStatus="NEW");仅差异点:
  280. /// - SourceType 标识为自动监控来源
  281. /// - 填入 SourceRuleId / SourceDataSourceId / SourcePayload / RelatedObjectCode 追溯
  282. /// - ExceptionTypeCode 固定 EQUIP_FAULT(G-01 首版唯一映射)
  283. /// - SceneCode 固定 S2(G-01 首版唯一场景,迁移后从 S2S6_PRODUCTION 切到单模块 S2)
  284. /// 不做补偿、重试、对账;失败由调用方接住。
  285. /// </summary>
  286. public async Task<AdoS8Exception> CreateFromWatchAsync(S8WatchHitResult hit)
  287. {
  288. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  289. throw new S8BizException("自动建单缺失追溯键");
  290. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  291. var title = $"[自动] 设备 {hit.RelatedObjectCode} {hit.TriggerCondition} {hit.ThresholdValue}(当前 {hit.CurrentValue})";
  292. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:固定 SceneCode=S2 + ExceptionTypeCode=EQUIP_FAULT,
  293. // 派生链路通过 ResolveModuleCodeAsync 严格按 S1-S7 走(结果稳定为 S2,但消除 FromScene 的 legacy 兼容路径)。
  294. var resolvedModule = await ResolveModuleCodeAsync(
  295. explicitModuleCode: null,
  296. hitModuleCode: null,
  297. sceneCode: S8SceneCode.S2,
  298. exceptionTypeCode: "EQUIP_FAULT");
  299. if (resolvedModule == null)
  300. {
  301. _logger.LogWarning(
  302. "auto_watch_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} ruleId={RuleId}",
  303. S8SceneCode.S2, "EQUIP_FAULT", hit.SourceRuleId);
  304. }
  305. var entity = new AdoS8Exception
  306. {
  307. // 租户/工厂:与 S8WatchSchedulerService.RunOnceAsync 当前固定上下文一致。
  308. TenantId = 1,
  309. FactoryId = 1,
  310. ExceptionCode = code,
  311. Title = title,
  312. Description = null,
  313. SceneCode = S8SceneCode.S2,
  314. // 首版自动监控建单来源标识(字符串值,先不抽常量类)。
  315. SourceType = "AUTO_WATCH",
  316. Status = "NEW",
  317. Severity = S8SeverityCode.Normalize(hit.Severity),
  318. PriorityScore = 0,
  319. PriorityLevel = "P3",
  320. // 首版兜底口径:Hit 未提供部门时置 0 仅为保证“能建成标准异常单并进入主链”,
  321. // 不是最终业务部门语义;后续需由上游查询结果提供,或在专项任务中补口径。
  322. OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
  323. ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
  324. ReporterId = null,
  325. CreatedAt = DateTime.Now,
  326. IsDeleted = false,
  327. // G-01 首版唯一异常类型映射(baseline 已迁后 EQUIP_FAULT 属 S2 制造协同场景)。
  328. ExceptionTypeCode = "EQUIP_FAULT",
  329. ModuleCode = resolvedModule,
  330. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  331. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  332. ProcessNodeCode = null,
  333. // 追溯三件套(自动建单必填口径)。
  334. SourceRuleId = hit.SourceRuleId,
  335. SourceDataSourceId = hit.DataSourceId,
  336. SourcePayload = hit.SourcePayload,
  337. RelatedObjectCode = hit.RelatedObjectCode
  338. };
  339. await _rep.AsTenant().UseTranAsync(async () =>
  340. {
  341. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  342. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  343. {
  344. ExceptionId = entity.Id,
  345. ActionCode = "CREATE",
  346. ActionLabel = "创建",
  347. FromStatus = null,
  348. ToStatus = "NEW",
  349. OperatorId = null,
  350. ActionRemark = "自动建单",
  351. CreatedAt = DateTime.Now
  352. });
  353. }, ex => throw ex);
  354. await TryStartIntakeFlowAsync(entity);
  355. return entity;
  356. }
  357. /// <summary>
  358. /// R2 自动建单分支(TIMEOUT 等新 evaluator 走此路径)。
  359. /// 与 <see cref="CreateFromWatchAsync"/> 并列:复用同一仓储 / 事务 / 时间线 ActionCode;
  360. /// 差异点:消费 <see cref="S8RuleHit"/> 一份命中模型,把 R2 新列(DedupKey / LastDetectedAt /
  361. /// SourceRuleCode / SourceObjectType / SourceObjectId)落齐;ExceptionTypeCode 由 hit 自带,
  362. /// 不再硬编码 EQUIP_FAULT。RecoveredAt 本轮不写。
  363. /// 调用方负责前置检查 ExceptionTypeCode 是否在 baseline;本方法不再二次校验。
  364. /// </summary>
  365. public async Task<AdoS8Exception> CreateFromHitAsync(S8RuleHit hit)
  366. {
  367. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  368. throw new S8BizException("自动建单缺失追溯键");
  369. var effectiveScene = string.IsNullOrWhiteSpace(hit.SceneCode) ? S8SceneCode.S2 : hit.SceneCode;
  370. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:R2 自动建单 module_code 派生统一走严格 S1-S7 链路;
  371. // hit.ModuleCode(evaluator 显式)→ rule/hit.SceneCode(当前 DB 100% S1-S7)→ exception_type.scene_code。
  372. var resolvedModule = await ResolveModuleCodeAsync(
  373. explicitModuleCode: null,
  374. hitModuleCode: hit.ModuleCode,
  375. sceneCode: effectiveScene,
  376. exceptionTypeCode: hit.ExceptionTypeCode);
  377. if (resolvedModule == null)
  378. {
  379. _logger.LogWarning(
  380. "auto_watch_hit_module_code_unresolved sceneCode={SceneCode} hitModule={HitModule} typeCode={TypeCode} ruleCode={RuleCode}",
  381. hit.SceneCode, hit.ModuleCode, hit.ExceptionTypeCode, hit.SourceRuleCode);
  382. }
  383. var code = $"EX-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  384. var entity = new AdoS8Exception
  385. {
  386. // 与 CreateFromWatchAsync 当前固定上下文一致;R2 不变更租户/工厂上下文语义。
  387. TenantId = 1,
  388. FactoryId = 1,
  389. ExceptionCode = code,
  390. Title = string.IsNullOrWhiteSpace(hit.Title)
  391. ? $"[自动] {hit.SourceObjectType} {hit.SourceObjectId}"
  392. : hit.Title,
  393. Description = null,
  394. SceneCode = effectiveScene,
  395. SourceType = "AUTO_WATCH",
  396. Status = "NEW",
  397. Severity = S8SeverityCode.Normalize(hit.Severity),
  398. PriorityScore = 0,
  399. PriorityLevel = "P3",
  400. OccurrenceDeptId = hit.OccurrenceDeptId ?? 0,
  401. ResponsibleDeptId = hit.ResponsibleDeptId ?? 0,
  402. ReporterId = null,
  403. CreatedAt = DateTime.Now,
  404. IsDeleted = false,
  405. ExceptionTypeCode = hit.ExceptionTypeCode,
  406. ModuleCode = resolvedModule,
  407. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  408. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  409. ProcessNodeCode = null,
  410. SourceRuleId = hit.SourceRuleId,
  411. SourceDataSourceId = hit.DataSourceId == 0 ? null : hit.DataSourceId,
  412. SourcePayload = hit.SourcePayload,
  413. RelatedObjectCode = hit.RelatedObjectCode,
  414. // R2 新列回填
  415. DedupKey = hit.DedupKey,
  416. LastDetectedAt = hit.DetectedAt,
  417. RecoveredAt = null,
  418. SourceRuleCode = hit.SourceRuleCode,
  419. SourceObjectType = hit.SourceObjectType,
  420. SourceObjectId = hit.SourceObjectId
  421. };
  422. await _rep.AsTenant().UseTranAsync(async () =>
  423. {
  424. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  425. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  426. {
  427. ExceptionId = entity.Id,
  428. ActionCode = "CREATE",
  429. ActionLabel = "创建",
  430. FromStatus = null,
  431. ToStatus = "NEW",
  432. OperatorId = null,
  433. ActionRemark = "自动建单(R2)",
  434. CreatedAt = DateTime.Now
  435. });
  436. }, ex => throw ex);
  437. await TryStartIntakeFlowAsync(entity);
  438. return entity;
  439. }
  440. public async Task<AdoS8Exception?> GetAsync(long id) =>
  441. await _rep.GetByIdAsync(id);
  442. public async Task<AdoS8Evidence> AddAttachmentAsync(long id, AdoS8AttachmentCreateDto dto)
  443. {
  444. var entity = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
  445. ?? throw new S8BizException("异常不存在");
  446. if (string.IsNullOrWhiteSpace(dto.FileName) || string.IsNullOrWhiteSpace(dto.FileUrl))
  447. throw new S8BizException("附件名称和地址必填");
  448. var evidence = new AdoS8Evidence
  449. {
  450. ExceptionId = id,
  451. EvidenceType = string.IsNullOrWhiteSpace(dto.EvidenceType) ? "file" : dto.EvidenceType,
  452. FileName = dto.FileName.Trim(),
  453. FileUrl = dto.FileUrl.Trim(),
  454. SourceSystem = dto.SourceSystem,
  455. UploadedBy = dto.UploadedBy,
  456. UploadedAt = DateTime.Now,
  457. IsDeleted = false
  458. };
  459. await _evidenceRep.InsertAsync(evidence);
  460. return evidence;
  461. }
  462. }