S8TaskFlowService.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  4. using Admin.NET.Plugin.ApprovalFlow;
  5. using Admin.NET.Plugin.ApprovalFlow.Service;
  6. using Microsoft.Extensions.Logging;
  7. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  8. public class S8TaskFlowService : ITransient
  9. {
  10. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  11. private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
  12. private readonly SqlSugarRepository<AdoS0EmployeeMaster> _employeeRep;
  13. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  14. private readonly SqlSugarRepository<ApprovalFlowInstance> _flowInstanceRep;
  15. private readonly SqlSugarRepository<ApprovalFlowTask> _flowTaskRep;
  16. private readonly FlowEngineService _flowEngine;
  17. private readonly UserManager _userManager;
  18. private readonly S8NotificationLayerResolver _notificationLayerResolver;
  19. private readonly ILogger<S8TaskFlowService> _logger;
  20. public S8TaskFlowService(
  21. SqlSugarRepository<AdoS8Exception> rep,
  22. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  23. SqlSugarRepository<AdoS0EmployeeMaster> employeeRep,
  24. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  25. SqlSugarRepository<ApprovalFlowInstance> flowInstanceRep,
  26. SqlSugarRepository<ApprovalFlowTask> flowTaskRep,
  27. FlowEngineService flowEngine,
  28. UserManager userManager,
  29. S8NotificationLayerResolver notificationLayerResolver,
  30. ILogger<S8TaskFlowService> logger)
  31. {
  32. _rep = rep;
  33. _timelineRep = timelineRep;
  34. _employeeRep = employeeRep;
  35. _deptRep = deptRep;
  36. _flowInstanceRep = flowInstanceRep;
  37. _flowTaskRep = flowTaskRep;
  38. _flowEngine = flowEngine;
  39. _userManager = userManager;
  40. _notificationLayerResolver = notificationLayerResolver;
  41. _logger = logger;
  42. }
  43. public async Task<AdoS8Exception> ClaimAsync(long id, long tenantId, long factoryId, long assigneeId, string? remark)
  44. {
  45. if (assigneeId <= 0) throw new S8BizException("认领需指定处理人 AssigneeId");
  46. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  47. if (!S8StatusRules.IsAllowedTransition(e.Status, "ASSIGNED"))
  48. throw new S8BizException($"状态 {e.Status} 不可认领");
  49. var fromStatus = e.Status;
  50. e.Status = "ASSIGNED";
  51. e.AssigneeId = assigneeId;
  52. e.AssignedAt = DateTime.Now;
  53. e.UpdatedAt = DateTime.Now;
  54. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):派单同步处理部门 = 处理人所属部门 RecID。
  55. // 兜底策略:解析失败保留原 ResponsibleDeptId,仅 warn;避免误覆盖既有归属。
  56. var resolvedDeptId = await ResolveEmployeeResponsibleDeptIdAsync(assigneeId, factoryId);
  57. if (resolvedDeptId.HasValue)
  58. {
  59. e.ResponsibleDeptId = resolvedDeptId.Value;
  60. }
  61. else
  62. {
  63. _logger.LogWarning(
  64. "s8_claim_dept_unresolved exceptionId={Id} assigneeId={AssigneeId} factoryId={FactoryId}",
  65. e.Id, assigneeId, factoryId);
  66. }
  67. await _rep.AsTenant().UseTranAsync(async () =>
  68. {
  69. await _rep.UpdateAsync(e);
  70. await InsertTimelineAsync(e.Id, "CLAIM", "认领", fromStatus, "ASSIGNED", assigneeId, null, remark);
  71. }, ex => throw ex);
  72. // 注:认领仅承接异常,不视为审批流完成。审批流的"通过"在"开始处理"那一步触发。
  73. return e;
  74. }
  75. public async Task<AdoS8Exception> TransferAsync(long id, long tenantId, long factoryId, long newAssigneeId, string? remark)
  76. {
  77. var allowedStatuses = new HashSet<string> { "ASSIGNED", "IN_PROGRESS" };
  78. if (newAssigneeId <= 0) throw new S8BizException("转派目标无效");
  79. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  80. if (e.ActiveFlowInstanceId.HasValue)
  81. throw new S8BizException("审批进行中不可转派");
  82. if (e.Status == "ESCALATED")
  83. throw new S8BizException("升级审批中不可转派");
  84. if (S8StatusRules.IsTerminal(e.Status))
  85. throw new S8BizException("已关闭不可转派");
  86. if (!allowedStatuses.Contains(e.Status))
  87. throw new S8BizException($"状态 {e.Status} 不可转派");
  88. e.AssigneeId = newAssigneeId;
  89. e.UpdatedAt = DateTime.Now;
  90. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):转派同步处理部门 = 新处理人所属部门 RecID。
  91. // 兜底策略:解析失败保留原 ResponsibleDeptId,仅 warn;避免误覆盖既有归属。
  92. var resolvedDeptId = await ResolveEmployeeResponsibleDeptIdAsync(newAssigneeId, factoryId);
  93. if (resolvedDeptId.HasValue)
  94. {
  95. e.ResponsibleDeptId = resolvedDeptId.Value;
  96. }
  97. else
  98. {
  99. _logger.LogWarning(
  100. "s8_transfer_dept_unresolved exceptionId={Id} newAssigneeId={AssigneeId} factoryId={FactoryId}",
  101. e.Id, newAssigneeId, factoryId);
  102. }
  103. await _rep.AsTenant().UseTranAsync(async () =>
  104. {
  105. await _rep.UpdateAsync(e);
  106. await InsertTimelineAsync(e.Id, "TRANSFER", "转派", e.Status, e.Status, newAssigneeId, null, remark);
  107. }, ex => throw ex);
  108. // 双线合一:S8 转派 = TB001 任务转办给新处理人。
  109. await TryTransferIntakeOnTransferAsync(e.Id, newAssigneeId, remark);
  110. return e;
  111. }
  112. private async Task TryTransferIntakeOnTransferAsync(long exceptionId, long newAssigneeRecId, string? remark)
  113. {
  114. try
  115. {
  116. var instance = await _flowInstanceRep.AsQueryable()
  117. .Where(x => x.BizType == "EXCEPTION_REPORT"
  118. && x.BizId == exceptionId
  119. && x.Status == FlowInstanceStatusEnum.Running)
  120. .FirstAsync();
  121. if (instance == null) return;
  122. var currentUserId = _userManager.UserId;
  123. var task = await _flowTaskRep.AsQueryable()
  124. .Where(x => x.InstanceId == instance.Id
  125. && x.AssigneeId == currentUserId
  126. && x.Status == FlowTaskStatusEnum.Pending)
  127. .FirstAsync();
  128. if (task == null) return;
  129. // newAssigneeRecId 是 EmployeeMaster.RecID;FlowEngine.Transfer 要 SysUser.UserId。
  130. var targetSysUserId = await _employeeRep.AsQueryable().ClearFilter()
  131. .Where(x => x.Id == newAssigneeRecId && x.SysUserId != null)
  132. .Select(x => x.SysUserId)
  133. .FirstAsync();
  134. if (targetSysUserId == null || targetSysUserId == 0)
  135. {
  136. _logger.LogWarning(
  137. "S8 转派联动审批流跳过:员工 {RecId} 未绑定 SysUser,TB001 任务保留原 assignee",
  138. newAssigneeRecId);
  139. return;
  140. }
  141. await _flowEngine.Transfer(task.Id, targetSysUserId.Value, remark ?? "S8 转派(双线合一自动转办)");
  142. }
  143. catch (Exception ex)
  144. {
  145. _logger.LogWarning(ex,
  146. "S8 转派时自动转办 TB001 任务失败 exceptionId={Id} newAssigneeRecId={AssigneeId}",
  147. exceptionId, newAssigneeRecId);
  148. }
  149. }
  150. public async Task<AdoS8Exception> StartProgressAsync(long id, long tenantId, long factoryId, string? remark)
  151. {
  152. var currentUserId = GetCurrentUserId();
  153. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  154. if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS"))
  155. throw new S8BizException($"状态 {e.Status} 不可开始处理");
  156. var fromStatus = e.Status;
  157. e.Status = "IN_PROGRESS";
  158. e.UpdatedAt = DateTime.Now;
  159. await _rep.AsTenant().UseTranAsync(async () =>
  160. {
  161. await _rep.UpdateAsync(e);
  162. await InsertTimelineAsync(e.Id, "START_PROGRESS", "开始处理", fromStatus, "IN_PROGRESS", currentUserId, null, remark);
  163. }, ex => throw ex);
  164. // 双线合一:开始处理 = TB001 异常提报审批通过。
  165. // 当前用户必须是 TB001 task 的 AssigneeId,FlowEngine 强校验。
  166. await TryApproveIntakeOnStartProgressAsync(e.Id, currentUserId);
  167. return e;
  168. }
  169. private async Task TryApproveIntakeOnStartProgressAsync(long exceptionId, long currentUserId)
  170. {
  171. try
  172. {
  173. var instance = await _flowInstanceRep.AsQueryable()
  174. .Where(x => x.BizType == "EXCEPTION_REPORT"
  175. && x.BizId == exceptionId
  176. && x.Status == FlowInstanceStatusEnum.Running)
  177. .FirstAsync();
  178. if (instance == null) return;
  179. var task = await _flowTaskRep.AsQueryable()
  180. .Where(x => x.InstanceId == instance.Id
  181. && x.AssigneeId == currentUserId
  182. && x.Status == FlowTaskStatusEnum.Pending)
  183. .FirstAsync();
  184. if (task == null) return;
  185. await _flowEngine.Approve(task.Id, "S8 已开始处理(双线合一自动同意)");
  186. }
  187. catch (Exception ex)
  188. {
  189. _logger.LogWarning(ex,
  190. "S8 开始处理时自动同意 TB001 任务失败 exceptionId={Id} userId={UserId}",
  191. exceptionId, currentUserId);
  192. }
  193. }
  194. public async Task<AdoS8Exception> UpgradeAsync(long id, long tenantId, long factoryId, string? remark)
  195. {
  196. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  197. if (e.ActiveFlowInstanceId.HasValue)
  198. throw new S8BizException("该异常已有进行中的审批流程,请等待审批完成");
  199. if (!S8StatusRules.IsAllowedTransition(e.Status, "ESCALATED"))
  200. throw new S8BizException($"状态 {e.Status} 不可升级");
  201. await _flowEngine.StartFlow(new StartFlowInput
  202. {
  203. BizType = "EXCEPTION_ESCALATION",
  204. BizId = e.Id,
  205. Title = $"异常升级审批 - {e.ExceptionCode}",
  206. Comment = remark,
  207. BizData = new Dictionary<string, object>
  208. {
  209. ["severity"] = e.Severity,
  210. ["sceneCode"] = e.SceneCode,
  211. ["priorityLevel"] = e.PriorityLevel,
  212. }
  213. });
  214. // 状态和时间线由 ExceptionEscalationBizHandler.OnFlowStarted 回调更新
  215. return await LoadAsync(id, tenantId, factoryId) ?? e;
  216. }
  217. public async Task<AdoS8Exception> RejectAsync(long id, long tenantId, long factoryId, string? remark)
  218. {
  219. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  220. if (!S8StatusRules.IsAllowedTransition(e.Status, "REJECTED"))
  221. throw new S8BizException($"状态 {e.Status} 不可驳回");
  222. var from = e.Status;
  223. e.Status = "REJECTED";
  224. e.UpdatedAt = DateTime.Now;
  225. await _rep.AsTenant().UseTranAsync(async () =>
  226. {
  227. await _rep.UpdateAsync(e);
  228. await InsertTimelineAsync(e.Id, "REJECT", "驳回", from, "REJECTED", null, null, remark);
  229. }, ex => throw ex);
  230. // 双线合一:S8 驳回 = TB001 流程整体拒绝(取消所有 pending 任务、Instance 终止)。
  231. await TryRejectIntakeOnRejectAsync(e.Id, remark);
  232. return e;
  233. }
  234. private async Task TryRejectIntakeOnRejectAsync(long exceptionId, string? remark)
  235. {
  236. try
  237. {
  238. var instance = await _flowInstanceRep.AsQueryable()
  239. .Where(x => x.BizType == "EXCEPTION_REPORT"
  240. && x.BizId == exceptionId
  241. && x.Status == FlowInstanceStatusEnum.Running)
  242. .FirstAsync();
  243. if (instance == null) return;
  244. var currentUserId = _userManager.UserId;
  245. var task = await _flowTaskRep.AsQueryable()
  246. .Where(x => x.InstanceId == instance.Id
  247. && x.AssigneeId == currentUserId
  248. && x.Status == FlowTaskStatusEnum.Pending)
  249. .FirstAsync();
  250. if (task == null) return;
  251. await _flowEngine.Reject(task.Id, remark ?? "S8 已驳回(双线合一自动拒绝)");
  252. }
  253. catch (Exception ex)
  254. {
  255. _logger.LogWarning(ex,
  256. "S8 驳回时自动拒绝 TB001 流程失败 exceptionId={Id}",
  257. exceptionId);
  258. }
  259. }
  260. public async Task<AdoS8Exception> SubmitVerificationAsync(
  261. long id, long tenantId, long factoryId,
  262. long verifierId, string? remark)
  263. {
  264. var currentUserId = GetCurrentUserId();
  265. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  266. await EnsureCurrentUserIsOperatorAsync(e.AssigneeId, currentUserId,
  267. "只有当前处理人才能提交复检(或当前账号未绑定员工主数据)");
  268. if (verifierId <= 0)
  269. throw new S8BizException("请选择检验人");
  270. if (!S8StatusRules.IsAllowedTransition(e.Status, "PENDING_VERIFICATION"))
  271. throw new S8BizException($"状态 {e.Status} 不可提交复检");
  272. var from = e.Status;
  273. e.Status = "PENDING_VERIFICATION";
  274. e.VerifierId = verifierId;
  275. e.VerificationAssignedAt = DateTime.Now;
  276. e.UpdatedAt = DateTime.Now;
  277. await _rep.AsTenant().UseTranAsync(async () =>
  278. {
  279. await _rep.UpdateAsync(e);
  280. await InsertTimelineAsync(e.Id, "VERIFY_SUBMITTED", "提交复检", from, "PENDING_VERIFICATION",
  281. currentUserId, null, remark);
  282. }, ex => throw ex);
  283. // 双线合一:提交复检 = 启动 EXCEPTION_CLOSURE 流程,指派检验人。
  284. // 该流程定义复用,作为复检/关闭确认通用审批载体;handler 只做 ActiveFlowInstanceId 维护。
  285. await TryStartVerificationFlowAsync(e, verifierId, remark);
  286. return await LoadAsync(id, tenantId, factoryId) ?? e;
  287. }
  288. private async Task TryStartVerificationFlowAsync(AdoS8Exception e, long verifierRecId, string? remark)
  289. {
  290. try
  291. {
  292. await _flowEngine.StartFlow(new StartFlowInput
  293. {
  294. BizType = "EXCEPTION_CLOSURE",
  295. BizId = e.Id,
  296. BizNo = e.ExceptionCode,
  297. Title = $"异常复检 - {e.ExceptionCode}",
  298. Comment = remark,
  299. BizData = new Dictionary<string, object>
  300. {
  301. ["sceneCode"] = e.SceneCode ?? string.Empty,
  302. ["verifierRecId"] = verifierRecId,
  303. }
  304. });
  305. }
  306. catch (Exception ex)
  307. {
  308. _logger.LogWarning(ex,
  309. "S8 提交复检时启动 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} verifierRecId={VerifierId}",
  310. e.Id, verifierRecId);
  311. }
  312. }
  313. public async Task<AdoS8Exception> ApproveVerificationAsync(
  314. long id, long tenantId, long factoryId,
  315. string? remark)
  316. {
  317. var currentUserId = GetCurrentUserId();
  318. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  319. await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId,
  320. "只有指定检验人才能检验通过(或当前账号未绑定员工主数据)");
  321. if (!S8StatusRules.IsAllowedTransition(e.Status, "CLOSED"))
  322. throw new S8BizException($"状态 {e.Status} 不可检验通过");
  323. var from = e.Status;
  324. e.Status = "CLOSED";
  325. e.VerifiedAt = DateTime.Now;
  326. e.VerificationResult = "APPROVED";
  327. e.VerificationRemark = remark;
  328. e.ClosedAt = DateTime.Now;
  329. e.UpdatedAt = DateTime.Now;
  330. await _rep.AsTenant().UseTranAsync(async () =>
  331. {
  332. await _rep.UpdateAsync(e);
  333. await InsertTimelineAsync(e.Id, "VERIFY_APPROVED", "检验通过", from, "CLOSED",
  334. currentUserId, null, remark);
  335. }, ex => throw ex);
  336. // 双线合一:检验通过 = EXCEPTION_CLOSURE 复检流程审批通过。
  337. await TryApproveVerificationFlowAsync(e.Id, currentUserId);
  338. // S8-R03-OVERDUE-CLOSE-NOTICE-1:闭环及时性提醒。
  339. // 触发条件:closedAt > slaDeadline;与 TimeoutFlag 运行时口径分离,仅对已关闭异常做闭环回顾。
  340. // 主流程已 commit(含状态机 + Timeline),异常隔离仅 LogWarning,不影响 CloseAsync。
  341. await TryDispatchOverdueCloseNotificationAsync(e);
  342. return e;
  343. }
  344. /// <summary>
  345. /// S8-R03-OVERDUE-CLOSE-NOTICE-1:异常关闭后若 closedAt &gt; slaDeadline 触发一条独立"超时关闭"通知。
  346. /// 复用 <see cref="S8NotificationLayerResolver.DispatchByLayerAsync"/> 分层链路,不写 ApprovalFlowNotifyLog。
  347. /// 与 <see cref="S8TimeoutAutoEscalationService"/>(未关闭超时升级)语义分离:本钩子仅在已关闭后回顾闭环及时性。
  348. /// 任何异常仅 LogWarning,不抛回 ApproveVerificationAsync 主流程。
  349. /// </summary>
  350. private async Task TryDispatchOverdueCloseNotificationAsync(AdoS8Exception e)
  351. {
  352. if (e == null || e.Id <= 0) return;
  353. if (e.ClosedAt == null || e.SlaDeadline == null) return;
  354. if (e.ClosedAt.Value <= e.SlaDeadline.Value) return;
  355. try
  356. {
  357. var overdueCloseHours = Math.Round(
  358. (decimal)(e.ClosedAt.Value - e.SlaDeadline.Value).TotalHours, 1);
  359. var sceneCode = string.IsNullOrWhiteSpace(e.SceneCode) ? "S8_DEMO_DEFAULT" : e.SceneCode;
  360. // S8-SEVERITY-FOLLOW-SERIOUS-STANDARDIZE-EXEC-1:派发前 Normalize 落 FOLLOW/SERIOUS。
  361. var severity = S8SeverityCode.Normalize(e.Severity);
  362. var content =
  363. $"异常 {e.ExceptionCode} 已关闭,但关闭时间超过 SLA 截止时间,超时关闭 {overdueCloseHours.ToString("0.#", System.Globalization.CultureInfo.InvariantCulture)} 小时,请关注闭环及时性。";
  364. await _notificationLayerResolver.DispatchByLayerAsync(new S8NotificationLayerResolver.DispatchByLayerInput
  365. {
  366. TenantId = e.TenantId,
  367. FactoryId = e.FactoryId,
  368. ExceptionId = e.Id,
  369. ExceptionNo = e.ExceptionCode,
  370. SceneCode = sceneCode,
  371. Severity = severity,
  372. Title = $"【超时关闭】{e.ExceptionCode}",
  373. Content = content,
  374. Status = e.Status,
  375. SourceRuleCode = e.SourceRuleCode,
  376. JumpUrl = $"/aidop/s8/exceptions/{e.Id}",
  377. OverdueClosed = true,
  378. ClosedAt = e.ClosedAt,
  379. SlaDeadlineRef = e.SlaDeadline,
  380. OverdueCloseHours = overdueCloseHours,
  381. });
  382. }
  383. catch (Exception ex)
  384. {
  385. _logger.LogWarning(ex, "notify_overdue_close_dispatch_throw exceptionId={Id}", e.Id);
  386. }
  387. }
  388. private async Task TryApproveVerificationFlowAsync(long exceptionId, long currentUserId)
  389. {
  390. try
  391. {
  392. var instance = await _flowInstanceRep.AsQueryable()
  393. .Where(x => x.BizType == "EXCEPTION_CLOSURE"
  394. && x.BizId == exceptionId
  395. && x.Status == FlowInstanceStatusEnum.Running)
  396. .FirstAsync();
  397. if (instance == null) return;
  398. var task = await _flowTaskRep.AsQueryable()
  399. .Where(x => x.InstanceId == instance.Id
  400. && x.AssigneeId == currentUserId
  401. && x.Status == FlowTaskStatusEnum.Pending)
  402. .FirstAsync();
  403. if (task == null) return;
  404. await _flowEngine.Approve(task.Id, "S8 检验通过(双线合一自动同意)");
  405. }
  406. catch (Exception ex)
  407. {
  408. _logger.LogWarning(ex,
  409. "S8 检验通过时自动同意 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}",
  410. exceptionId, currentUserId);
  411. }
  412. }
  413. public async Task<AdoS8Exception> RejectVerificationAsync(
  414. long id, long tenantId, long factoryId,
  415. string remark)
  416. {
  417. var currentUserId = GetCurrentUserId();
  418. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  419. await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId,
  420. "只有指定检验人才能检验退回(或当前账号未绑定员工主数据)");
  421. if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS"))
  422. throw new S8BizException($"状态 {e.Status} 不可检验退回");
  423. if (string.IsNullOrWhiteSpace(remark))
  424. throw new S8BizException("检验退回必须填写退回原因");
  425. var from = e.Status;
  426. e.Status = "IN_PROGRESS";
  427. e.VerifiedAt = DateTime.Now;
  428. e.VerificationResult = "REJECTED";
  429. e.VerificationRemark = remark;
  430. e.UpdatedAt = DateTime.Now;
  431. await _rep.AsTenant().UseTranAsync(async () =>
  432. {
  433. await _rep.UpdateAsync(e);
  434. await InsertTimelineAsync(e.Id, "VERIFY_REJECTED", "检验退回", from, "IN_PROGRESS",
  435. currentUserId, null, remark);
  436. }, ex => throw ex);
  437. // 双线合一:检验退回 = EXCEPTION_CLOSURE 复检流程整体拒绝。
  438. await TryRejectVerificationFlowAsync(e.Id, currentUserId, remark);
  439. return e;
  440. }
  441. private async Task TryRejectVerificationFlowAsync(long exceptionId, long currentUserId, string? remark)
  442. {
  443. try
  444. {
  445. var instance = await _flowInstanceRep.AsQueryable()
  446. .Where(x => x.BizType == "EXCEPTION_CLOSURE"
  447. && x.BizId == exceptionId
  448. && x.Status == FlowInstanceStatusEnum.Running)
  449. .FirstAsync();
  450. if (instance == null) return;
  451. var task = await _flowTaskRep.AsQueryable()
  452. .Where(x => x.InstanceId == instance.Id
  453. && x.AssigneeId == currentUserId
  454. && x.Status == FlowTaskStatusEnum.Pending)
  455. .FirstAsync();
  456. if (task == null) return;
  457. await _flowEngine.Reject(task.Id, remark ?? "S8 检验退回(双线合一自动拒绝)");
  458. }
  459. catch (Exception ex)
  460. {
  461. _logger.LogWarning(ex,
  462. "S8 检验退回时自动拒绝 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}",
  463. exceptionId, currentUserId);
  464. }
  465. }
  466. public async Task CommentAsync(long id, string? remark)
  467. {
  468. var e = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
  469. ?? throw new S8BizException("异常不存在");
  470. await InsertTimelineAsync(e.Id, "COMMENT", "补充说明", e.Status, e.Status, null, null, remark);
  471. }
  472. private Task<AdoS8Exception?> LoadAsync(long id, long tenantId, long factoryId) =>
  473. _rep.GetFirstAsync(x => x.Id == id && x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted);
  474. // 统一复用框架登录上下文,避免业务身份继续信任前端传参。
  475. private long GetCurrentUserId()
  476. {
  477. var currentUserId = _userManager.UserId;
  478. if (currentUserId <= 0)
  479. throw new S8BizException("未获取到当前登录用户");
  480. return currentUserId;
  481. }
  482. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):处理人 RecID → 所属部门 RecID。
  483. // 链路:EmployeeMaster.Department(codename) + FactoryRefId → DepartmentMaster.Department + FactoryRefId → RecID。
  484. // ClearFilter 同口径(S8MasterDataAdapter / GetEmployeeSysUserIdAsync),factoryId 做硬边界。
  485. // 解析失败返回 null —— 调用方按"保留原值 + warn"处理,绝不覆盖既有 ResponsibleDeptId。
  486. private async Task<long?> ResolveEmployeeResponsibleDeptIdAsync(long assigneeId, long factoryId)
  487. {
  488. if (assigneeId <= 0 || factoryId <= 0) return null;
  489. var emp = await _employeeRep.AsQueryable().ClearFilter()
  490. .Where(x => x.Id == assigneeId && x.FactoryRefId == factoryId)
  491. .Select(x => new { x.Department })
  492. .FirstAsync();
  493. if (emp == null || string.IsNullOrWhiteSpace(emp.Department)) return null;
  494. var deptId = await _deptRep.AsQueryable().ClearFilter()
  495. .Where(x => x.Department == emp.Department && x.FactoryRefId == factoryId && x.IsActive)
  496. .Select(x => (long?)x.Id)
  497. .FirstAsync();
  498. return deptId;
  499. }
  500. // 把异常上的处理人/检验人(employeeId) 经 EmployeeMaster.SysUserId 解析到系统账号 ID。
  501. // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位,必须 ClearFilter 跳过多租户全局 filter;
  502. // 通过 employee.Id 主键精确查询作为安全边界,无跨租户泄漏风险。
  503. private async Task<long?> GetEmployeeSysUserIdAsync(long? employeeId)
  504. {
  505. if (!employeeId.HasValue || employeeId.Value <= 0) return null;
  506. var emp = await _employeeRep.AsQueryable().ClearFilter()
  507. .Where(x => x.Id == employeeId.Value)
  508. .FirstAsync();
  509. return emp?.SysUserId;
  510. }
  511. // 鉴权统一入口:要求当前登录用户必须是 employeeId 解析后的 SysUserId。
  512. private async Task EnsureCurrentUserIsOperatorAsync(long? employeeId, long currentUserId, string failMessage)
  513. {
  514. var ownerSysUserId = await GetEmployeeSysUserIdAsync(employeeId);
  515. if (ownerSysUserId != currentUserId)
  516. throw new S8BizException(failMessage);
  517. }
  518. private async Task InsertTimelineAsync(long exceptionId, string code, string label, string? from, string? to,
  519. long? operatorId, string? operatorName, string? remark) =>
  520. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  521. {
  522. ExceptionId = exceptionId,
  523. ActionCode = code,
  524. ActionLabel = label,
  525. FromStatus = from,
  526. ToStatus = to,
  527. OperatorId = operatorId,
  528. OperatorName = operatorName,
  529. ActionRemark = remark,
  530. CreatedAt = DateTime.Now
  531. });
  532. }