ExceptionClosureBizHandler.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.ApprovalFlow;
  3. using Admin.NET.Plugin.ApprovalFlow.Service;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using Microsoft.Extensions.Logging;
  6. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  7. /// <summary>
  8. /// EXCEPTION_CLOSURE 流程的 Biz 回调(双线合一后已退化为复检审批载体)。
  9. /// 启动点:S8 提交复检;终态由 S8 业务侧(ApproveVerification/RejectVerification)决定。
  10. /// 该 handler 只负责维护 ActiveFlowInstanceId 字段,不再改写 e.Status。
  11. /// </summary>
  12. public class ExceptionClosureBizHandler : IFlowBizHandler, ITransient
  13. {
  14. public string BizType => "EXCEPTION_CLOSURE";
  15. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  16. private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
  17. private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
  18. private readonly SqlSugarRepository<SysUser> _userRep;
  19. private readonly ILogger<ExceptionClosureBizHandler> _logger;
  20. public ExceptionClosureBizHandler(
  21. SqlSugarRepository<AdoS8Exception> rep,
  22. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  23. SqlSugarRepository<ApprovalFlowTask> taskRep,
  24. SqlSugarRepository<SysUser> userRep,
  25. ILogger<ExceptionClosureBizHandler> logger)
  26. {
  27. _rep = rep;
  28. _timelineRep = timelineRep;
  29. _taskRep = taskRep;
  30. _userRep = userRep;
  31. _logger = logger;
  32. }
  33. public async Task OnFlowStarted(long bizId, long instanceId)
  34. {
  35. var e = await _rep.GetByIdAsync(bizId) ?? throw new S8BizException("异常不存在");
  36. e.ActiveFlowInstanceId = instanceId;
  37. e.ActiveFlowBizType = BizType;
  38. e.UpdatedAt = DateTime.Now;
  39. await _rep.UpdateAsync(e);
  40. await AlignTaskToSelectedVerifierAsync(e, instanceId);
  41. await InsertTimelineAsync(e.Id, "VERIFY_FLOW_START", "复检审批流启动", instanceId, null);
  42. }
  43. /// <summary>
  44. /// S8-RESPONSIBILITY-POOL-1:把复检任务对齐到<b>页面上选定的那个检验人</b>。
  45. ///
  46. /// <para><b>要解决的错位</b>:<c>FlowEngineService.ResolveApprovers</c> 只看流程定义节点的
  47. /// <c>ApproverType</c> / <c>ApproverIds</c>,<b>完全不读 <c>BizData</c></b> ——
  48. /// 提交复核时传进去的 <c>verifierUserId</c> 从来没有参与过审批人解析。
  49. /// 于是「<c>verifier_user_id</c>(业务状态机 authority)」与
  50. /// 「<c>ApprovalFlowTask.AssigneeId</c>(审批任务 authority)」是两条各走各的链:
  51. /// UI 选了甲,任务却发给节点角色里的那一组人。当前 UAT 之所以看起来正常,
  52. /// 只是因为该角色恰好只有一名成员 —— 一旦角色多一个人,错位立刻出现且两边都不报错。</para>
  53. ///
  54. /// <para><b>为什么在这里对齐</b>:<c>StartFlow</c> 的顺序是
  55. /// <c>ProcessNextNode</c>(建任务)→ <c>OnFlowStarted</c>(本回调),
  56. /// 任务此刻已存在,S8 可以在自己的流程实例上把它指到正确的人。
  57. /// <b>不改 FlowEngine</b> —— 那是 S1/S5/S6/S7 共用的平台组件,
  58. /// 为 S8 一家加一条 BizData 动态审批人分支,风险面远超收益。</para>
  59. ///
  60. /// <para><b>节点默认或签</b>(一人通过即完成、其余 Pending 自动取消),
  61. /// 因此把该节点的待办收敛成"选定检验人一个人"不改变流程语义。
  62. /// 多余的待办直接删除而不是留着:留着就等于池外的人仍然能点通过,
  63. /// 而 S8 侧的 <c>verifier_user_id</c> 校验又会拒绝他 —— 回到两边互相卡住的老形态。</para>
  64. ///
  65. /// <para>对齐失败<b>不回滚</b>已提交的复核(状态与时间线已落库),但必须落 Warning:
  66. /// 静默失败会让"任务发错人"重新变成无从查起的问题。</para>
  67. /// </summary>
  68. private async Task AlignTaskToSelectedVerifierAsync(AdoS8Exception e, long instanceId)
  69. {
  70. try
  71. {
  72. if (e.VerifierUserId is not > 0) return;
  73. var verifierId = e.VerifierUserId.Value;
  74. var pending = await _taskRep.AsQueryable().ClearFilter()
  75. .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
  76. .ToListAsync();
  77. if (pending.Count == 0) return;
  78. // 已经就是这个人:不写库、不留噪音日志。
  79. if (pending.Count == 1 && pending[0].AssigneeId == verifierId) return;
  80. var verifier = await _userRep.AsQueryable().ClearFilter()
  81. .Where(u => u.Id == verifierId).FirstAsync();
  82. var keep = pending.FirstOrDefault(t => t.AssigneeId == verifierId) ?? pending[0];
  83. var drop = pending.Where(t => t.Id != keep.Id).Select(t => t.Id).ToList();
  84. if (keep.AssigneeId != verifierId)
  85. {
  86. keep.AssigneeId = verifierId;
  87. keep.AssigneeName = verifier?.RealName ?? verifier?.Account;
  88. await _taskRep.AsUpdateable(keep)
  89. .UpdateColumns(t => new { t.AssigneeId, t.AssigneeName })
  90. .ExecuteCommandAsync();
  91. }
  92. if (drop.Count > 0)
  93. await _taskRep.AsDeleteable().Where(t => drop.Contains(t.Id)).ExecuteCommandAsync();
  94. _logger.LogInformation(
  95. "s8_verify_task_aligned exceptionId={Id} instanceId={Instance} verifierUserId={Verifier} kept={Kept} dropped={Dropped}",
  96. e.Id, instanceId, verifierId, keep.Id, drop.Count);
  97. }
  98. catch (Exception ex)
  99. {
  100. _logger.LogWarning(ex,
  101. "s8_verify_task_align_failed exceptionId={Id} instanceId={Instance} verifierUserId={Verifier};"
  102. + "复核已提交但审批任务可能仍指向流程定义里的角色成员,需人工核对",
  103. e.Id, instanceId, e.VerifierUserId);
  104. }
  105. }
  106. public async Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId)
  107. {
  108. var e = await _rep.GetByIdAsync(bizId) ?? throw new S8BizException("异常不存在");
  109. e.ActiveFlowInstanceId = null;
  110. e.ActiveFlowBizType = null;
  111. e.UpdatedAt = DateTime.Now;
  112. await _rep.UpdateAsync(e);
  113. // 状态由 S8 服务层管控(VERIFY_APPROVED/VERIFY_REJECTED 时间线已写)。
  114. // 这里只补一条流程级审计标记,便于追溯审批实例终态。
  115. var label = finalStatus switch
  116. {
  117. FlowInstanceStatusEnum.Approved => "复检审批流通过",
  118. FlowInstanceStatusEnum.Rejected => "复检审批流拒绝",
  119. FlowInstanceStatusEnum.Cancelled => "复检审批流撤回",
  120. _ => "复检审批流结束",
  121. };
  122. await InsertTimelineAsync(e.Id, "VERIFY_FLOW_END", label, instanceId, lastApproverId);
  123. }
  124. public async Task<Dictionary<string, object>> GetBizData(long bizId)
  125. {
  126. var e = await _rep.GetByIdAsync(bizId);
  127. if (e == null) return new Dictionary<string, object>();
  128. return new Dictionary<string, object>
  129. {
  130. ["sceneCode"] = e.SceneCode ?? string.Empty,
  131. ["factoryCode"] = e.FactoryId.ToString(),
  132. };
  133. }
  134. private async Task InsertTimelineAsync(long exceptionId, string code, string label,
  135. long? instanceId, long? approverId)
  136. {
  137. string? remark = null;
  138. if (instanceId.HasValue && approverId.HasValue)
  139. remark = $"审批实例ID: {instanceId},审批人: {approverId}";
  140. else if (instanceId.HasValue)
  141. remark = $"审批实例ID: {instanceId}";
  142. else if (approverId.HasValue)
  143. remark = $"审批人: {approverId}";
  144. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  145. {
  146. ExceptionId = exceptionId,
  147. ActionCode = code,
  148. ActionLabel = label,
  149. FromStatus = null,
  150. ToStatus = null,
  151. OperatorUserId = approverId,
  152. ActionRemark = remark,
  153. CreatedAt = DateTime.Now
  154. });
  155. }
  156. }