S8ManualReportService.cs 21 KB

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