S8ManualReportService.cs 21 KB

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