using Admin.NET.Core;
using Admin.NET.Plugin.ApprovalFlow;
using Admin.NET.Plugin.ApprovalFlow.Service;
using Admin.NET.Plugin.AiDOP.Entity.S8;
using Microsoft.Extensions.Logging;
namespace Admin.NET.Plugin.AiDOP.Service.S8;
///
/// EXCEPTION_CLOSURE 流程的 Biz 回调(双线合一后已退化为复检审批载体)。
/// 启动点:S8 提交复检;终态由 S8 业务侧(ApproveVerification/RejectVerification)决定。
/// 该 handler 只负责维护 ActiveFlowInstanceId 字段,不再改写 e.Status。
///
public class ExceptionClosureBizHandler : IFlowBizHandler, ITransient
{
public string BizType => "EXCEPTION_CLOSURE";
private readonly SqlSugarRepository _rep;
private readonly SqlSugarRepository _timelineRep;
private readonly SqlSugarRepository _taskRep;
private readonly SqlSugarRepository _userRep;
private readonly ILogger _logger;
public ExceptionClosureBizHandler(
SqlSugarRepository rep,
SqlSugarRepository timelineRep,
SqlSugarRepository taskRep,
SqlSugarRepository userRep,
ILogger logger)
{
_rep = rep;
_timelineRep = timelineRep;
_taskRep = taskRep;
_userRep = userRep;
_logger = logger;
}
public async Task OnFlowStarted(long bizId, long instanceId)
{
var e = await _rep.GetByIdAsync(bizId) ?? throw new S8BizException("异常不存在");
e.ActiveFlowInstanceId = instanceId;
e.ActiveFlowBizType = BizType;
e.UpdatedAt = DateTime.Now;
await _rep.UpdateAsync(e);
await AlignTaskToSelectedVerifierAsync(e, instanceId);
await InsertTimelineAsync(e.Id, "VERIFY_FLOW_START", "复检审批流启动", instanceId, null);
}
///
/// S8-RESPONSIBILITY-POOL-1:把复检任务对齐到页面上选定的那个检验人。
///
/// 要解决的错位:FlowEngineService.ResolveApprovers 只看流程定义节点的
/// ApproverType / ApproverIds,完全不读 BizData ——
/// 提交复核时传进去的 verifierUserId 从来没有参与过审批人解析。
/// 于是「verifier_user_id(业务状态机 authority)」与
/// 「ApprovalFlowTask.AssigneeId(审批任务 authority)」是两条各走各的链:
/// UI 选了甲,任务却发给节点角色里的那一组人。当前 UAT 之所以看起来正常,
/// 只是因为该角色恰好只有一名成员 —— 一旦角色多一个人,错位立刻出现且两边都不报错。
///
/// 为什么在这里对齐:StartFlow 的顺序是
/// ProcessNextNode(建任务)→ OnFlowStarted(本回调),
/// 任务此刻已存在,S8 可以在自己的流程实例上把它指到正确的人。
/// 不改 FlowEngine —— 那是 S1/S5/S6/S7 共用的平台组件,
/// 为 S8 一家加一条 BizData 动态审批人分支,风险面远超收益。
///
/// 节点默认或签(一人通过即完成、其余 Pending 自动取消),
/// 因此把该节点的待办收敛成"选定检验人一个人"不改变流程语义。
/// 多余的待办直接删除而不是留着:留着就等于池外的人仍然能点通过,
/// 而 S8 侧的 verifier_user_id 校验又会拒绝他 —— 回到两边互相卡住的老形态。
///
/// 对齐失败不回滚已提交的复核(状态与时间线已落库),但必须落 Warning:
/// 静默失败会让"任务发错人"重新变成无从查起的问题。
///
private async Task AlignTaskToSelectedVerifierAsync(AdoS8Exception e, long instanceId)
{
try
{
if (e.VerifierUserId is not > 0) return;
var verifierId = e.VerifierUserId.Value;
var pending = await _taskRep.AsQueryable().ClearFilter()
.Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
.ToListAsync();
if (pending.Count == 0) return;
// 已经就是这个人:不写库、不留噪音日志。
if (pending.Count == 1 && pending[0].AssigneeId == verifierId) return;
var verifier = await _userRep.AsQueryable().ClearFilter()
.Where(u => u.Id == verifierId).FirstAsync();
var keep = pending.FirstOrDefault(t => t.AssigneeId == verifierId) ?? pending[0];
var drop = pending.Where(t => t.Id != keep.Id).Select(t => t.Id).ToList();
if (keep.AssigneeId != verifierId)
{
keep.AssigneeId = verifierId;
keep.AssigneeName = verifier?.RealName ?? verifier?.Account;
await _taskRep.AsUpdateable(keep)
.UpdateColumns(t => new { t.AssigneeId, t.AssigneeName })
.ExecuteCommandAsync();
}
if (drop.Count > 0)
await _taskRep.AsDeleteable().Where(t => drop.Contains(t.Id)).ExecuteCommandAsync();
_logger.LogInformation(
"s8_verify_task_aligned exceptionId={Id} instanceId={Instance} verifierUserId={Verifier} kept={Kept} dropped={Dropped}",
e.Id, instanceId, verifierId, keep.Id, drop.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"s8_verify_task_align_failed exceptionId={Id} instanceId={Instance} verifierUserId={Verifier};"
+ "复核已提交但审批任务可能仍指向流程定义里的角色成员,需人工核对",
e.Id, instanceId, e.VerifierUserId);
}
}
public async Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId)
{
var e = await _rep.GetByIdAsync(bizId) ?? throw new S8BizException("异常不存在");
e.ActiveFlowInstanceId = null;
e.ActiveFlowBizType = null;
e.UpdatedAt = DateTime.Now;
await _rep.UpdateAsync(e);
// 状态由 S8 服务层管控(VERIFY_APPROVED/VERIFY_REJECTED 时间线已写)。
// 这里只补一条流程级审计标记,便于追溯审批实例终态。
var label = finalStatus switch
{
FlowInstanceStatusEnum.Approved => "复检审批流通过",
FlowInstanceStatusEnum.Rejected => "复检审批流拒绝",
FlowInstanceStatusEnum.Cancelled => "复检审批流撤回",
_ => "复检审批流结束",
};
await InsertTimelineAsync(e.Id, "VERIFY_FLOW_END", label, instanceId, lastApproverId);
}
public async Task> GetBizData(long bizId)
{
var e = await _rep.GetByIdAsync(bizId);
if (e == null) return new Dictionary();
return new Dictionary
{
["sceneCode"] = e.SceneCode ?? string.Empty,
["factoryCode"] = e.FactoryId.ToString(),
};
}
private async Task InsertTimelineAsync(long exceptionId, string code, string label,
long? instanceId, long? approverId)
{
string? remark = null;
if (instanceId.HasValue && approverId.HasValue)
remark = $"审批实例ID: {instanceId},审批人: {approverId}";
else if (instanceId.HasValue)
remark = $"审批实例ID: {instanceId}";
else if (approverId.HasValue)
remark = $"审批人: {approverId}";
await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
{
ExceptionId = exceptionId,
ActionCode = code,
ActionLabel = label,
FromStatus = null,
ToStatus = null,
OperatorUserId = approverId,
ActionRemark = remark,
CreatedAt = DateTime.Now
});
}
}