S8ManualReportService.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  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. // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):自动建单部门 resolver 需读取 watch_rule.params_json 里的默认部门字段。
  70. private readonly SqlSugarRepository<AdoS8WatchRule> _ruleRep;
  71. private readonly UserManager _userManager;
  72. private readonly FlowEngineService _flowEngine;
  73. private readonly ILogger<S8ManualReportService> _logger;
  74. public S8ManualReportService(
  75. SqlSugarRepository<AdoS8Exception> rep,
  76. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  77. SqlSugarRepository<AdoS8Evidence> evidenceRep,
  78. SqlSugarRepository<AdoS8SceneConfig> sceneRep,
  79. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  80. SqlSugarRepository<AdoS0LineMaster> lineRep,
  81. SqlSugarRepository<AdoS8ExceptionType> typeRep,
  82. SqlSugarRepository<AdoS0EmployeeMaster> empRep,
  83. SqlSugarRepository<AdoS8WatchRule> ruleRep,
  84. UserManager userManager,
  85. FlowEngineService flowEngine,
  86. ILogger<S8ManualReportService> logger)
  87. {
  88. _rep = rep;
  89. _timelineRep = timelineRep;
  90. _evidenceRep = evidenceRep;
  91. _sceneRep = sceneRep;
  92. _deptRep = deptRep;
  93. _lineRep = lineRep;
  94. _typeRep = typeRep;
  95. _empRep = empRep;
  96. _ruleRep = ruleRep;
  97. _userManager = userManager;
  98. _flowEngine = flowEngine;
  99. _logger = logger;
  100. }
  101. // S8-SLA-TIMEOUT-RUNTIME-1(P3):按 exception_type.sla_minutes 计算 sla_deadline。
  102. // typeCode 空 / type 缺失 / sla_minutes <= 0 → 返回 null(不阻断建单,仅 LogWarning)。
  103. // 不写 timeout_flag;timeout_flag 已降级为 legacy 字段,当前超时由读端基于 sla_deadline + status 在线计算。
  104. private async Task<DateTime?> ResolveSlaDeadlineAsync(string? exceptionTypeCode, DateTime createdAt)
  105. {
  106. if (string.IsNullOrWhiteSpace(exceptionTypeCode)) return null;
  107. var slaMinutes = await _typeRep.AsQueryable().ClearFilter()
  108. .Where(t => t.TypeCode == exceptionTypeCode)
  109. .OrderByDescending(t => t.FactoryId)
  110. .Select(t => (int?)t.SlaMinutes)
  111. .FirstAsync();
  112. if (slaMinutes == null)
  113. {
  114. _logger.LogWarning("s8_sla_type_not_found exceptionTypeCode={TypeCode}", exceptionTypeCode);
  115. return null;
  116. }
  117. if (slaMinutes.Value <= 0) return null;
  118. return createdAt.AddMinutes(slaMinutes.Value);
  119. }
  120. // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):自动建单部门解析顺序 hit → watch_rule.params_json default → 未归属。
  121. // 单字段独立解析(occurrence / responsible 各自走优先级)。任一字段最终为 null → 调用方按"throw 让 scheduler 跳过"处理。
  122. // 不允许写 0 作为最终值;不硬编码 1=质量部 / 2=生产部;不猜测业务对象部门派生(待后续增强)。
  123. // 协议常量:未归属部门 codename = D-UNASSIGNED;按 factory_ref_id 唯一存在;本批不创建该基线,缺失时 resolver 返回 null。
  124. private const string UnassignedDepartmentCode = "D-UNASSIGNED";
  125. private sealed class AutoWatchDeptResolution
  126. {
  127. public long? OccurrenceDeptId { get; set; }
  128. public long? ResponsibleDeptId { get; set; }
  129. public string OccurrenceSource { get; set; } = "failed";
  130. public string ResponsibleSource { get; set; } = "failed";
  131. }
  132. private async Task<AutoWatchDeptResolution> ResolveAutoWatchDepartmentsAsync(
  133. string path,
  134. long factoryId,
  135. long? hitOccurrenceDeptId,
  136. long? hitResponsibleDeptId,
  137. long? ruleId,
  138. string? ruleCode,
  139. string? moduleCode,
  140. string? sceneCode,
  141. string? exceptionTypeCode,
  142. string? sourceObjectType,
  143. string? sourceObjectId,
  144. string? relatedObjectCode,
  145. string? dedupKey)
  146. {
  147. var result = new AutoWatchDeptResolution();
  148. // 1) hit dept 优先
  149. var occOk = await ValidateDeptForFactoryAsync(hitOccurrenceDeptId, factoryId);
  150. var respOk = await ValidateDeptForFactoryAsync(hitResponsibleDeptId, factoryId);
  151. if (occOk) { result.OccurrenceDeptId = hitOccurrenceDeptId; result.OccurrenceSource = "hit"; }
  152. if (respOk) { result.ResponsibleDeptId = hitResponsibleDeptId; result.ResponsibleSource = "hit"; }
  153. // 2) watch_rule.params_json 默认部门
  154. if ((!occOk || !respOk) && ruleId.HasValue && ruleId.Value > 0)
  155. {
  156. var paramsJson = await _ruleRep.AsQueryable()
  157. .Where(x => x.Id == ruleId.Value)
  158. .Select(x => x.ParamsJson)
  159. .FirstAsync();
  160. var (paramsOcc, paramsResp) = ParseParamsDefaultDepts(paramsJson);
  161. if (!occOk && paramsOcc.HasValue)
  162. {
  163. if (await ValidateDeptForFactoryAsync(paramsOcc, factoryId))
  164. {
  165. result.OccurrenceDeptId = paramsOcc;
  166. result.OccurrenceSource = "watch_rule_params";
  167. }
  168. else
  169. {
  170. _logger.LogWarning(
  171. "s8_auto_watch_default_dept_invalid path={Path} ruleId={RuleId} ruleCode={RuleCode} field=defaultOccurrenceDeptId value={Value} factoryId={FactoryId}",
  172. path, ruleId, ruleCode, paramsOcc, factoryId);
  173. }
  174. }
  175. if (!respOk && paramsResp.HasValue)
  176. {
  177. if (await ValidateDeptForFactoryAsync(paramsResp, factoryId))
  178. {
  179. result.ResponsibleDeptId = paramsResp;
  180. result.ResponsibleSource = "watch_rule_params";
  181. }
  182. else
  183. {
  184. _logger.LogWarning(
  185. "s8_auto_watch_default_dept_invalid path={Path} ruleId={RuleId} ruleCode={RuleCode} field=defaultResponsibleDeptId value={Value} factoryId={FactoryId}",
  186. path, ruleId, ruleCode, paramsResp, factoryId);
  187. }
  188. }
  189. }
  190. // 3) 未归属部门 fallback
  191. long? unassignedId = null;
  192. if (result.OccurrenceDeptId == null || result.ResponsibleDeptId == null)
  193. {
  194. unassignedId = await _deptRep.AsQueryable().ClearFilter()
  195. .Where(x => x.Department == UnassignedDepartmentCode && x.FactoryRefId == factoryId && x.IsActive)
  196. .Select(x => (long?)x.Id)
  197. .FirstAsync();
  198. if (result.OccurrenceDeptId == null && unassignedId.HasValue)
  199. {
  200. result.OccurrenceDeptId = unassignedId;
  201. result.OccurrenceSource = "unassigned";
  202. _logger.LogWarning(
  203. "s8_auto_watch_dept_unassigned_fallback path={Path} ruleId={RuleId} ruleCode={RuleCode} field=occurrence factoryId={FactoryId} unassignedDeptId={UnassignedId} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} relatedObjectCode={RelatedObjectCode} dedupKey={DedupKey}",
  204. path, ruleId, ruleCode, factoryId, unassignedId, sourceObjectType, sourceObjectId, relatedObjectCode, dedupKey);
  205. }
  206. if (result.ResponsibleDeptId == null && unassignedId.HasValue)
  207. {
  208. result.ResponsibleDeptId = unassignedId;
  209. result.ResponsibleSource = "unassigned";
  210. _logger.LogWarning(
  211. "s8_auto_watch_dept_unassigned_fallback path={Path} ruleId={RuleId} ruleCode={RuleCode} field=responsible factoryId={FactoryId} unassignedDeptId={UnassignedId} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} relatedObjectCode={RelatedObjectCode} dedupKey={DedupKey}",
  212. path, ruleId, ruleCode, factoryId, unassignedId, sourceObjectType, sourceObjectId, relatedObjectCode, dedupKey);
  213. }
  214. }
  215. return result;
  216. }
  217. private async Task<bool> ValidateDeptForFactoryAsync(long? deptId, long factoryId)
  218. {
  219. if (!deptId.HasValue || deptId.Value <= 0 || factoryId <= 0) return false;
  220. return await _deptRep.AsQueryable().ClearFilter()
  221. .Where(x => x.Id == deptId.Value && x.FactoryRefId == factoryId && x.IsActive)
  222. .AnyAsync();
  223. }
  224. private static (long? Occurrence, long? Responsible) ParseParamsDefaultDepts(string? paramsJson)
  225. {
  226. if (string.IsNullOrWhiteSpace(paramsJson)) return (null, null);
  227. try
  228. {
  229. using var doc = System.Text.Json.JsonDocument.Parse(paramsJson);
  230. long? occ = ReadJsonLong(doc.RootElement, "defaultOccurrenceDeptId");
  231. long? resp = ReadJsonLong(doc.RootElement, "defaultResponsibleDeptId");
  232. return (occ, resp);
  233. }
  234. catch (System.Text.Json.JsonException) { return (null, null); }
  235. }
  236. private static long? ReadJsonLong(System.Text.Json.JsonElement root, string property)
  237. {
  238. if (root.ValueKind != System.Text.Json.JsonValueKind.Object) return null;
  239. if (!root.TryGetProperty(property, out var el)) return null;
  240. if (el.ValueKind == System.Text.Json.JsonValueKind.Number && el.TryGetInt64(out var v)) return v;
  241. if (el.ValueKind == System.Text.Json.JsonValueKind.String && long.TryParse(el.GetString(), out var sv)) return sv;
  242. return null;
  243. }
  244. // S8-REPORTER-IDSPACE-FIX-1(P0-B-1):把当前登录 SysUser.Id 反查 EmployeeMaster.RecID。
  245. // 用于 reporter_id idspace 与 assignee_id / verifier_id 对齐。
  246. // 协议:sys_user_id + factory_ref_id 双键命中;EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位 →
  247. // ClearFilter,由 factoryRefId 做硬边界(与 S8MasterDataAdapter / S8TaskFlowService 同口径)。
  248. // 未绑定时返回 null,调用方负责诊断日志。
  249. private async Task<long?> ResolveCurrentEmployeeIdAsync(long factoryId, long sysUserId)
  250. {
  251. if (sysUserId <= 0 || factoryId <= 0) return null;
  252. var emp = await _empRep.AsQueryable().ClearFilter()
  253. .Where(x => x.SysUserId == sysUserId && x.FactoryRefId == factoryId && x.IsActive)
  254. .Select(x => new { x.Id })
  255. .FirstAsync();
  256. return emp?.Id;
  257. }
  258. // S8-MANUAL-REPORT-DEFAULT-OCCURRENCE-DEPT-1(P1):取当前登录员工的业务部门作为默认发生部门。
  259. // 链路:SysUser.Id → EmployeeMaster.sys_user_id → EmployeeMaster.Department(string)
  260. // + EmployeeMaster.FactoryRefId → DepartmentMaster.Department + FactoryRefId → RecID。
  261. // 不绑定 / 部门为空 / 部门未匹配 / 命中多条:均返回 (null,null) + warning,绝不报错也不阻断页面。
  262. // 仅作为前端 form 默认初值;用户仍可改,CreateAsync 的 dept=0 guard 不变。
  263. private async Task<(long? DeptId, string? DeptName)> ResolveCurrentEmployeeDefaultDeptAsync(long factoryId, long sysUserId)
  264. {
  265. if (sysUserId <= 0 || factoryId <= 0) return (null, null);
  266. var emp = await _empRep.AsQueryable().ClearFilter()
  267. .Where(x => x.SysUserId == sysUserId && x.FactoryRefId == factoryId && x.IsActive)
  268. .Select(x => new { x.Id, x.Department, x.FactoryRefId })
  269. .FirstAsync();
  270. if (emp == null)
  271. {
  272. _logger.LogWarning(
  273. "s8_manual_report_default_dept_unbound_employee sysUserId={SysUserId} factoryId={FactoryId}",
  274. sysUserId, factoryId);
  275. return (null, null);
  276. }
  277. if (string.IsNullOrWhiteSpace(emp.Department))
  278. {
  279. _logger.LogWarning(
  280. "s8_manual_report_default_dept_employee_dept_empty sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId}",
  281. sysUserId, factoryId, emp.Id);
  282. return (null, null);
  283. }
  284. var depts = await _deptRep.AsQueryable().ClearFilter()
  285. .Where(x => x.Department == emp.Department && x.FactoryRefId == emp.FactoryRefId && x.IsActive)
  286. .Select(x => new { x.Id, x.Department, x.Descr })
  287. .Take(2)
  288. .ToListAsync();
  289. if (depts.Count == 0)
  290. {
  291. _logger.LogWarning(
  292. "s8_manual_report_default_dept_unmatched sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId} departmentCode={DeptCode}",
  293. sysUserId, factoryId, emp.Id, emp.Department);
  294. return (null, null);
  295. }
  296. if (depts.Count > 1)
  297. {
  298. _logger.LogWarning(
  299. "s8_manual_report_default_dept_ambiguous sysUserId={SysUserId} factoryId={FactoryId} employeeId={EmployeeId} departmentCode={DeptCode} matchedCount={Count}",
  300. sysUserId, factoryId, emp.Id, emp.Department, depts.Count);
  301. return (null, null);
  302. }
  303. var d = depts[0];
  304. return (d.Id, string.IsNullOrWhiteSpace(d.Descr) ? d.Department : d.Descr);
  305. }
  306. /// <summary>
  307. /// 主动提报推断 ExceptionTypeCode:场景下取启用且 SortNo 最小的一条。
  308. /// baseline 异常类型当前 tenant_id=0/factory_id=0(全局基线),所以匹配条件为
  309. /// (tenantId 命中 OR 0) AND (factoryId 命中 OR 0)。ClearFilter 兜底全局多租户过滤器。
  310. /// 找不到返回 null(保持兼容)。
  311. /// </summary>
  312. private async Task<string?> InferExceptionTypeCodeAsync(long tenantId, long factoryId, string sceneCode)
  313. {
  314. if (string.IsNullOrWhiteSpace(sceneCode)) return null;
  315. return await _typeRep.AsQueryable().ClearFilter()
  316. .Where(x => (x.TenantId == tenantId || x.TenantId == 0)
  317. && (x.FactoryId == factoryId || x.FactoryId == 0)
  318. && x.SceneCode == sceneCode && x.Enabled)
  319. .OrderBy(x => x.SortNo)
  320. .Select(x => x.TypeCode)
  321. .FirstAsync();
  322. }
  323. /// <summary>
  324. /// TB001 异常提报审批流:自动监控 + 主动提报后软触发,失败仅 warn 日志,不阻断建单。
  325. /// </summary>
  326. private async Task TryStartIntakeFlowAsync(AdoS8Exception entity)
  327. {
  328. try
  329. {
  330. await _flowEngine.StartFlow(new StartFlowInput
  331. {
  332. BizType = "EXCEPTION_REPORT",
  333. BizId = entity.Id,
  334. BizNo = entity.ExceptionCode,
  335. Title = $"异常提报 - {entity.ExceptionCode}",
  336. Comment = entity.SourceType == "AUTO_WATCH" ? "自动监控触发" : "主动提报触发",
  337. BizData = new Dictionary<string, object>
  338. {
  339. ["sceneCode"] = entity.SceneCode ?? "",
  340. ["exceptionTypeCode"] = entity.ExceptionTypeCode ?? "",
  341. ["sourceType"] = entity.SourceType ?? ""
  342. }
  343. });
  344. }
  345. catch (Exception ex)
  346. {
  347. _logger.LogWarning(ex, "TB001 异常提报审批流触发失败 ExceptionId={Id} ExceptionCode={Code}", entity.Id, entity.ExceptionCode);
  348. }
  349. }
  350. public async Task<object> GetFormOptionsAsync(long tenantId, long factoryId)
  351. {
  352. var scenes = await _sceneRep.AsQueryable()
  353. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.Enabled)
  354. .OrderBy(x => x.SortNo)
  355. .Select(x => new { value = x.SceneCode, label = x.SceneName })
  356. .ToListAsync();
  357. // ClearFilter:DepartmentMaster.tenant_id 属 S0 域租户,不与登录 token TenantId 一致;
  358. // 用 factory_ref_id 做硬边界,安全等价。同 BUG-S8-EMPLOYEES-TENANT-FILTER 协议。
  359. var departments = await _deptRep.AsQueryable().ClearFilter()
  360. .Where(x => x.FactoryRefId == factoryId)
  361. .OrderBy(x => x.Department)
  362. .Take(500)
  363. .Select(x => new { value = x.Id, label = x.Descr ?? x.Department })
  364. .ToListAsync();
  365. var lines = await _lineRep.AsQueryable().ClearFilter()
  366. .Where(x => x.FactoryRefId == factoryId)
  367. .OrderBy(x => x.Line)
  368. .Take(500)
  369. .Select(x => new { value = x.Id, label = x.Describe ?? x.Line })
  370. .ToListAsync();
  371. // S8-MANUAL-REPORT-DEFAULT-OCCURRENCE-DEPT-1(P1):默认发生部门 = 当前登录员工的业务部门。
  372. // 未登录 / 未绑定 / 部门不可解析 → 返回 null,前端继续要求用户手动选择。
  373. var sysUserId = _userManager.UserId;
  374. var (defaultOccDeptId, defaultOccDeptName) = sysUserId > 0
  375. ? await ResolveCurrentEmployeeDefaultDeptAsync(factoryId, sysUserId)
  376. : (null, null);
  377. return new
  378. {
  379. scenes,
  380. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:业务枚举 FOLLOW/SERIOUS 两档。
  381. severities = S8SeverityCode.Options(),
  382. departments,
  383. lines,
  384. materials = Array.Empty<object>(),
  385. defaultOccurrenceDeptId = defaultOccDeptId,
  386. defaultOccurrenceDeptName = defaultOccDeptName,
  387. };
  388. }
  389. public async Task<AdoS8ManualReportResultDto> CreateAsync(AdoS8ManualReportCreateDto dto)
  390. {
  391. if (string.IsNullOrWhiteSpace(dto.Title)) throw new S8BizException("标题必填");
  392. if (string.IsNullOrWhiteSpace(dto.SceneCode)) throw new S8BizException("场景必填");
  393. // S8-MANUAL-REPORT-DEPT-ZERO-GUARD-1(P0-B-3):人工提报禁止 dept=0 入库。
  394. // 字段类型 long(非空),未填默认 0;guard 命中 <=0 直接拒绝,绝不转 null/默认部门/未归属。
  395. // 范围仅限本入口;自动建单(CreateFromWatchAsync/CreateFromHitAsync)的 ?? 0 风险归 P0-B-4。
  396. if (dto.OccurrenceDeptId <= 0) throw new S8BizException("发生部门不能为空,请选择有效发生部门");
  397. if (dto.ResponsibleDeptId <= 0) throw new S8BizException("处理部门不能为空,请选择有效处理部门");
  398. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:白名单接受新值(FOLLOW/SERIOUS)+ 旧值兼容;
  399. // 通过 Normalize 写入 DB 一律为 FOLLOW/SERIOUS。
  400. var rawSeverity = string.IsNullOrWhiteSpace(dto.Severity) ? S8SeverityCode.Follow : dto.Severity.Trim();
  401. if (!AllowedSeverities.Contains(rawSeverity))
  402. throw new S8BizException($"严重度 {rawSeverity} 非法,仅允许 FOLLOW/SERIOUS");
  403. var severity = S8SeverityCode.Normalize(rawSeverity);
  404. // 提报人以服务端登录上下文为准,忽略前端传入;未登录上下文落 null。
  405. // currentUserId 仍是 SysUser.Id,仅用作 timeline.OperatorId(与 S8TaskFlowService 同口径);
  406. // S8-REPORTER-IDSPACE-FIX-1:ReporterId 写入改用 EmployeeMaster.RecID(与 assignee/verifier 同 idspace)。
  407. var currentUserId = _userManager.UserId > 0 ? _userManager.UserId : (long?)null;
  408. long? currentEmployeeId = null;
  409. if (currentUserId.HasValue)
  410. {
  411. currentEmployeeId = await ResolveCurrentEmployeeIdAsync(dto.FactoryId, currentUserId.Value);
  412. if (currentEmployeeId == null)
  413. {
  414. _logger.LogWarning(
  415. "manual_report_reporter_unbound sysUserId={SysUserId} factoryId={FactoryId} title={Title}",
  416. currentUserId.Value, dto.FactoryId, dto.Title);
  417. }
  418. }
  419. // 主动提报无前端 type 字段,按场景兜底推断;保证不进"未分类"桶。
  420. var inferredType = await InferExceptionTypeCodeAsync(dto.TenantId, dto.FactoryId, dto.SceneCode.Trim());
  421. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:手工提报 module_code 严格按 S1-S7 派生;
  422. // dto 暂不携带显式 module_code,按 scene → exception_type.scene 链路降级。
  423. var resolvedModule = await ResolveModuleCodeAsync(
  424. explicitModuleCode: null,
  425. hitModuleCode: null,
  426. sceneCode: dto.SceneCode.Trim(),
  427. exceptionTypeCode: inferredType);
  428. if (resolvedModule == null)
  429. {
  430. _logger.LogWarning(
  431. "manual_report_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} title={Title}",
  432. dto.SceneCode, inferredType, dto.Title);
  433. }
  434. // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now,避免漂移。
  435. var now = DateTime.Now;
  436. var slaDeadline = await ResolveSlaDeadlineAsync(inferredType, now);
  437. var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  438. var entity = new AdoS8Exception
  439. {
  440. TenantId = dto.TenantId,
  441. FactoryId = dto.FactoryId,
  442. ExceptionCode = code,
  443. Title = dto.Title.Trim(),
  444. Description = dto.Description,
  445. SceneCode = dto.SceneCode.Trim(),
  446. SourceType = "MANUAL",
  447. Status = "NEW",
  448. Severity = severity,
  449. PriorityScore = 0,
  450. PriorityLevel = "P3",
  451. OccurrenceDeptId = dto.OccurrenceDeptId,
  452. ResponsibleDeptId = dto.ResponsibleDeptId,
  453. // S8-REPORTER-IDSPACE-FIX-1:reporter_id idspace = EmployeeMaster.RecID(与 assignee/verifier 同)。
  454. ReporterId = currentEmployeeId,
  455. ExceptionTypeCode = inferredType,
  456. ModuleCode = resolvedModule,
  457. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  458. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  459. ProcessNodeCode = null,
  460. // S8-MANUAL-RELATED-OBJECT-FILL-1:手工提报支持自由文本关联对象编码(订单项/类订单项),
  461. // 空白归 null;不写 source_object_type / source_object_id / dedup_key(仍由自动监控链路独占)。
  462. RelatedObjectCode = string.IsNullOrWhiteSpace(dto.RelatedObjectCode) ? null : dto.RelatedObjectCode.Trim(),
  463. CreatedAt = now,
  464. // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 由 exception_type.sla_minutes 决定;缺配置 → null。
  465. SlaDeadline = slaDeadline,
  466. IsDeleted = false
  467. };
  468. await _rep.AsTenant().UseTranAsync(async () =>
  469. {
  470. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  471. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  472. {
  473. ExceptionId = entity.Id,
  474. ActionCode = "CREATE",
  475. ActionLabel = "创建",
  476. FromStatus = null,
  477. ToStatus = "NEW",
  478. OperatorId = currentUserId,
  479. ActionRemark = "主动提报",
  480. CreatedAt = DateTime.Now
  481. });
  482. }, ex => throw ex);
  483. await TryStartIntakeFlowAsync(entity);
  484. return new AdoS8ManualReportResultDto
  485. {
  486. ExceptionId = entity.Id,
  487. ExceptionCode = entity.ExceptionCode,
  488. TaskId = entity.Id
  489. };
  490. }
  491. /// <summary>
  492. /// G01-06:自动建单分支(非第二套创建主链)。
  493. /// 这是本服务内的自动监控建单路径,与 <see cref="CreateAsync"/> 并列,
  494. /// 复用同一仓储(_rep / _timelineRep)、同一事务边界、同一 ExceptionCode 生成规则、
  495. /// 同一时间线主链(ActionCode="CREATE"、ToStatus="NEW");仅差异点:
  496. /// - SourceType 标识为自动监控来源
  497. /// - 填入 SourceRuleId / SourceDataSourceId / SourcePayload / RelatedObjectCode 追溯
  498. /// - ExceptionTypeCode 固定 EQUIP_FAULT(G-01 首版唯一映射)
  499. /// - SceneCode 固定 S2(G-01 首版唯一场景,迁移后从 S2S6_PRODUCTION 切到单模块 S2)
  500. /// 不做补偿、重试、对账;失败由调用方接住。
  501. /// </summary>
  502. public async Task<AdoS8Exception> CreateFromWatchAsync(S8WatchHitResult hit)
  503. {
  504. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  505. throw new S8BizException("自动建单缺失追溯键");
  506. // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now。
  507. var now = DateTime.Now;
  508. var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  509. var title = $"[自动] 设备 {hit.RelatedObjectCode} {hit.TriggerCondition} {hit.ThresholdValue}(当前 {hit.CurrentValue})";
  510. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:固定 SceneCode=S2 + ExceptionTypeCode=EQUIP_FAULT,
  511. // 派生链路通过 ResolveModuleCodeAsync 严格按 S1-S7 走(结果稳定为 S2,但消除 FromScene 的 legacy 兼容路径)。
  512. var resolvedModule = await ResolveModuleCodeAsync(
  513. explicitModuleCode: null,
  514. hitModuleCode: null,
  515. sceneCode: S8SceneCode.S2,
  516. exceptionTypeCode: "EQUIP_FAULT");
  517. if (resolvedModule == null)
  518. {
  519. _logger.LogWarning(
  520. "auto_watch_module_code_unresolved sceneCode={SceneCode} exceptionTypeCode={TypeCode} ruleId={RuleId}",
  521. S8SceneCode.S2, "EQUIP_FAULT", hit.SourceRuleId);
  522. }
  523. // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):部门解析顺序 hit → params → unassigned;resolver 失败 → throw。
  524. const long fixedTenantId = 1;
  525. const long fixedFactoryId = 1;
  526. var deptResolution = await ResolveAutoWatchDepartmentsAsync(
  527. path: nameof(CreateFromWatchAsync),
  528. factoryId: fixedFactoryId,
  529. hitOccurrenceDeptId: hit.OccurrenceDeptId,
  530. hitResponsibleDeptId: hit.ResponsibleDeptId,
  531. ruleId: hit.SourceRuleId,
  532. ruleCode: hit.SourceRuleCode,
  533. moduleCode: resolvedModule,
  534. sceneCode: S8SceneCode.S2,
  535. exceptionTypeCode: "EQUIP_FAULT",
  536. sourceObjectType: null,
  537. sourceObjectId: null,
  538. relatedObjectCode: hit.RelatedObjectCode,
  539. dedupKey: null);
  540. if (!deptResolution.OccurrenceDeptId.HasValue || !deptResolution.ResponsibleDeptId.HasValue)
  541. {
  542. _logger.LogWarning(
  543. "s8_auto_watch_dept_resolve_failed path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occSource={OccSource} respSource={RespSource} relatedObjectCode={RelatedObjectCode}",
  544. nameof(CreateFromWatchAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
  545. deptResolution.OccurrenceSource, deptResolution.ResponsibleSource, hit.RelatedObjectCode);
  546. throw new S8BizException($"自动建单部门解析失败:缺少未归属部门基线(factory_ref_id={fixedFactoryId}, codename={UnassignedDepartmentCode})");
  547. }
  548. var entity = new AdoS8Exception
  549. {
  550. // 租户/工厂:与 S8WatchSchedulerService.RunOnceAsync 当前固定上下文一致。
  551. TenantId = fixedTenantId,
  552. FactoryId = fixedFactoryId,
  553. ExceptionCode = code,
  554. Title = title,
  555. Description = null,
  556. SceneCode = S8SceneCode.S2,
  557. // 首版自动监控建单来源标识(字符串值,先不抽常量类)。
  558. SourceType = "AUTO_WATCH",
  559. Status = "NEW",
  560. Severity = S8SeverityCode.Normalize(hit.Severity),
  561. PriorityScore = 0,
  562. PriorityLevel = "P3",
  563. // S8-AUTO-WATCH-DEPT-RESOLVE-1:经 resolver 严格校验后写入;不再使用 ?? 0 兜底。
  564. OccurrenceDeptId = deptResolution.OccurrenceDeptId.Value,
  565. ResponsibleDeptId = deptResolution.ResponsibleDeptId.Value,
  566. ReporterId = null,
  567. CreatedAt = now,
  568. // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 由 exception_type.sla_minutes 决定(EQUIP_FAULT)。
  569. SlaDeadline = await ResolveSlaDeadlineAsync("EQUIP_FAULT", now),
  570. IsDeleted = false,
  571. // G-01 首版唯一异常类型映射(baseline 已迁后 EQUIP_FAULT 属 S2 制造协同场景)。
  572. ExceptionTypeCode = "EQUIP_FAULT",
  573. ModuleCode = resolvedModule,
  574. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  575. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  576. ProcessNodeCode = null,
  577. // 追溯三件套(自动建单必填口径)。
  578. SourceRuleId = hit.SourceRuleId,
  579. SourceDataSourceId = hit.DataSourceId,
  580. SourcePayload = hit.SourcePayload,
  581. RelatedObjectCode = hit.RelatedObjectCode
  582. };
  583. _logger.LogInformation(
  584. "s8_auto_watch_dept_resolved path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occDeptId={OccDeptId} occSource={OccSource} respDeptId={RespDeptId} respSource={RespSource}",
  585. nameof(CreateFromWatchAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
  586. entity.OccurrenceDeptId, deptResolution.OccurrenceSource, entity.ResponsibleDeptId, deptResolution.ResponsibleSource);
  587. await _rep.AsTenant().UseTranAsync(async () =>
  588. {
  589. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  590. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  591. {
  592. ExceptionId = entity.Id,
  593. ActionCode = "CREATE",
  594. ActionLabel = "创建",
  595. FromStatus = null,
  596. ToStatus = "NEW",
  597. OperatorId = null,
  598. ActionRemark = "自动建单",
  599. CreatedAt = DateTime.Now
  600. });
  601. }, ex => throw ex);
  602. await TryStartIntakeFlowAsync(entity);
  603. return entity;
  604. }
  605. /// <summary>
  606. /// R2 自动建单分支(TIMEOUT 等新 evaluator 走此路径)。
  607. /// 与 <see cref="CreateFromWatchAsync"/> 并列:复用同一仓储 / 事务 / 时间线 ActionCode;
  608. /// 差异点:消费 <see cref="S8RuleHit"/> 一份命中模型,把 R2 新列(DedupKey / LastDetectedAt /
  609. /// SourceRuleCode / SourceObjectType / SourceObjectId)落齐;ExceptionTypeCode 由 hit 自带,
  610. /// 不再硬编码 EQUIP_FAULT。RecoveredAt 本轮不写。
  611. /// 调用方负责前置检查 ExceptionTypeCode 是否在 baseline;本方法不再二次校验。
  612. /// </summary>
  613. public async Task<AdoS8Exception> CreateFromHitAsync(S8RuleHit hit)
  614. {
  615. if (hit.SourceRuleId <= 0 || string.IsNullOrWhiteSpace(hit.RelatedObjectCode))
  616. throw new S8BizException("自动建单缺失追溯键");
  617. var effectiveScene = string.IsNullOrWhiteSpace(hit.SceneCode) ? S8SceneCode.S2 : hit.SceneCode;
  618. // S8-EXCEPTION-CREATION-MODULE-CODE-FIX-1:R2 自动建单 module_code 派生统一走严格 S1-S7 链路;
  619. // hit.ModuleCode(evaluator 显式)→ rule/hit.SceneCode(当前 DB 100% S1-S7)→ exception_type.scene_code。
  620. var resolvedModule = await ResolveModuleCodeAsync(
  621. explicitModuleCode: null,
  622. hitModuleCode: hit.ModuleCode,
  623. sceneCode: effectiveScene,
  624. exceptionTypeCode: hit.ExceptionTypeCode);
  625. if (resolvedModule == null)
  626. {
  627. _logger.LogWarning(
  628. "auto_watch_hit_module_code_unresolved sceneCode={SceneCode} hitModule={HitModule} typeCode={TypeCode} ruleCode={RuleCode}",
  629. hit.SceneCode, hit.ModuleCode, hit.ExceptionTypeCode, hit.SourceRuleCode);
  630. }
  631. // S8-AUTO-WATCH-DEPT-RESOLVE-1(P2):部门解析顺序 hit → params → unassigned;resolver 失败 → throw。
  632. const long fixedTenantId = 1;
  633. const long fixedFactoryId = 1;
  634. var deptResolution = await ResolveAutoWatchDepartmentsAsync(
  635. path: nameof(CreateFromHitAsync),
  636. factoryId: fixedFactoryId,
  637. hitOccurrenceDeptId: hit.OccurrenceDeptId,
  638. hitResponsibleDeptId: hit.ResponsibleDeptId,
  639. ruleId: hit.SourceRuleId,
  640. ruleCode: hit.SourceRuleCode,
  641. moduleCode: resolvedModule,
  642. sceneCode: effectiveScene,
  643. exceptionTypeCode: hit.ExceptionTypeCode,
  644. sourceObjectType: hit.SourceObjectType,
  645. sourceObjectId: hit.SourceObjectId,
  646. relatedObjectCode: hit.RelatedObjectCode,
  647. dedupKey: hit.DedupKey);
  648. if (!deptResolution.OccurrenceDeptId.HasValue || !deptResolution.ResponsibleDeptId.HasValue)
  649. {
  650. _logger.LogWarning(
  651. "s8_auto_watch_dept_resolve_failed path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occSource={OccSource} respSource={RespSource} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} dedupKey={DedupKey}",
  652. nameof(CreateFromHitAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
  653. deptResolution.OccurrenceSource, deptResolution.ResponsibleSource, hit.SourceObjectType, hit.SourceObjectId, hit.DedupKey);
  654. throw new S8BizException($"自动建单部门解析失败:缺少未归属部门基线(factory_ref_id={fixedFactoryId}, codename={UnassignedDepartmentCode})");
  655. }
  656. // S8-SLA-TIMEOUT-RUNTIME-1(P3):CreatedAt 与 SlaDeadline 共用同一 now。
  657. var now = DateTime.Now;
  658. var code = $"EX-{now:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
  659. var slaDeadline = await ResolveSlaDeadlineAsync(hit.ExceptionTypeCode, now);
  660. var entity = new AdoS8Exception
  661. {
  662. // 与 CreateFromWatchAsync 当前固定上下文一致;R2 不变更租户/工厂上下文语义。
  663. TenantId = fixedTenantId,
  664. FactoryId = fixedFactoryId,
  665. ExceptionCode = code,
  666. Title = string.IsNullOrWhiteSpace(hit.Title)
  667. ? $"[自动] {hit.SourceObjectType} {hit.SourceObjectId}"
  668. : hit.Title,
  669. Description = null,
  670. SceneCode = effectiveScene,
  671. SourceType = "AUTO_WATCH",
  672. Status = "NEW",
  673. Severity = S8SeverityCode.Normalize(hit.Severity),
  674. PriorityScore = 0,
  675. PriorityLevel = "P3",
  676. // S8-AUTO-WATCH-DEPT-RESOLVE-1:经 resolver 严格校验后写入;不再使用 ?? 0 兜底。
  677. OccurrenceDeptId = deptResolution.OccurrenceDeptId.Value,
  678. ResponsibleDeptId = deptResolution.ResponsibleDeptId.Value,
  679. ReporterId = null,
  680. CreatedAt = now,
  681. // S8-SLA-TIMEOUT-RUNTIME-1:sla_deadline 来自 hit.ExceptionTypeCode 对应 sla_minutes。
  682. SlaDeadline = slaDeadline,
  683. IsDeleted = false,
  684. ExceptionTypeCode = hit.ExceptionTypeCode,
  685. ModuleCode = resolvedModule,
  686. // S8-PROCESS-NODE-MODULE-CODE-ALIGNMENT-EXEC-1:当前阶段 process_node_code 留空,
  687. // module_code 承担 S1-S7 主流程归属;process_node_code 留给未来更细流程节点。
  688. ProcessNodeCode = null,
  689. SourceRuleId = hit.SourceRuleId,
  690. SourceDataSourceId = hit.DataSourceId == 0 ? null : hit.DataSourceId,
  691. SourcePayload = hit.SourcePayload,
  692. RelatedObjectCode = hit.RelatedObjectCode,
  693. // R2 新列回填
  694. DedupKey = hit.DedupKey,
  695. LastDetectedAt = hit.DetectedAt,
  696. RecoveredAt = null,
  697. SourceRuleCode = hit.SourceRuleCode,
  698. SourceObjectType = hit.SourceObjectType,
  699. SourceObjectId = hit.SourceObjectId
  700. };
  701. _logger.LogInformation(
  702. "s8_auto_watch_dept_resolved path={Path} ruleId={RuleId} ruleCode={RuleCode} factoryId={FactoryId} occDeptId={OccDeptId} occSource={OccSource} respDeptId={RespDeptId} respSource={RespSource} sourceObjectType={SourceObjectType} sourceObjectId={SourceObjectId} dedupKey={DedupKey}",
  703. nameof(CreateFromHitAsync), hit.SourceRuleId, hit.SourceRuleCode, fixedFactoryId,
  704. entity.OccurrenceDeptId, deptResolution.OccurrenceSource, entity.ResponsibleDeptId, deptResolution.ResponsibleSource, hit.SourceObjectType, hit.SourceObjectId, hit.DedupKey);
  705. await _rep.AsTenant().UseTranAsync(async () =>
  706. {
  707. entity = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  708. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  709. {
  710. ExceptionId = entity.Id,
  711. ActionCode = "CREATE",
  712. ActionLabel = "创建",
  713. FromStatus = null,
  714. ToStatus = "NEW",
  715. OperatorId = null,
  716. ActionRemark = "自动建单(R2)",
  717. CreatedAt = DateTime.Now
  718. });
  719. }, ex => throw ex);
  720. await TryStartIntakeFlowAsync(entity);
  721. return entity;
  722. }
  723. public async Task<AdoS8Exception?> GetAsync(long id) =>
  724. await _rep.GetByIdAsync(id);
  725. public async Task<AdoS8Evidence> AddAttachmentAsync(long id, AdoS8AttachmentCreateDto dto)
  726. {
  727. var entity = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
  728. ?? throw new S8BizException("异常不存在");
  729. if (string.IsNullOrWhiteSpace(dto.FileName) || string.IsNullOrWhiteSpace(dto.FileUrl))
  730. throw new S8BizException("附件名称和地址必填");
  731. var evidence = new AdoS8Evidence
  732. {
  733. ExceptionId = id,
  734. EvidenceType = string.IsNullOrWhiteSpace(dto.EvidenceType) ? "file" : dto.EvidenceType,
  735. FileName = dto.FileName.Trim(),
  736. FileUrl = dto.FileUrl.Trim(),
  737. SourceSystem = dto.SourceSystem,
  738. UploadedBy = dto.UploadedBy,
  739. UploadedAt = DateTime.Now,
  740. IsDeleted = false
  741. };
  742. await _evidenceRep.InsertAsync(evidence);
  743. return evidence;
  744. }
  745. }