using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.ApprovalFlow; using Admin.NET.Plugin.ApprovalFlow.Service; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.Service.S8; public class S8TaskFlowService : ITransient { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _timelineRep; private readonly SqlSugarRepository _employeeRep; private readonly SqlSugarRepository _flowInstanceRep; private readonly SqlSugarRepository _flowTaskRep; private readonly FlowEngineService _flowEngine; private readonly UserManager _userManager; private readonly ILogger _logger; public S8TaskFlowService( SqlSugarRepository rep, SqlSugarRepository timelineRep, SqlSugarRepository employeeRep, SqlSugarRepository flowInstanceRep, SqlSugarRepository flowTaskRep, FlowEngineService flowEngine, UserManager userManager, ILogger logger) { _rep = rep; _timelineRep = timelineRep; _employeeRep = employeeRep; _flowInstanceRep = flowInstanceRep; _flowTaskRep = flowTaskRep; _flowEngine = flowEngine; _userManager = userManager; _logger = logger; } public async Task ClaimAsync(long id, long tenantId, long factoryId, long assigneeId, string? remark) { if (assigneeId <= 0) throw new S8BizException("认领需指定处理人 AssigneeId"); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); if (!S8StatusRules.IsAllowedTransition(e.Status, "ASSIGNED")) throw new S8BizException($"状态 {e.Status} 不可认领"); var fromStatus = e.Status; e.Status = "ASSIGNED"; e.AssigneeId = assigneeId; e.AssignedAt = DateTime.Now; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "CLAIM", "认领", fromStatus, "ASSIGNED", assigneeId, null, remark); }, ex => throw ex); // 注:认领仅承接异常,不视为审批流完成。审批流的"通过"在"开始处理"那一步触发。 return e; } public async Task TransferAsync(long id, long tenantId, long factoryId, long newAssigneeId, string? remark) { var allowedStatuses = new HashSet { "ASSIGNED", "IN_PROGRESS" }; if (newAssigneeId <= 0) throw new S8BizException("转派目标无效"); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); if (e.ActiveFlowInstanceId.HasValue) throw new S8BizException("审批进行中不可转派"); if (e.Status == "ESCALATED") throw new S8BizException("升级审批中不可转派"); if (S8StatusRules.IsTerminal(e.Status)) throw new S8BizException("已关闭不可转派"); if (!allowedStatuses.Contains(e.Status)) throw new S8BizException($"状态 {e.Status} 不可转派"); e.AssigneeId = newAssigneeId; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "TRANSFER", "转派", e.Status, e.Status, newAssigneeId, null, remark); }, ex => throw ex); // 双线合一:S8 转派 = TB001 任务转办给新处理人。 await TryTransferIntakeOnTransferAsync(e.Id, newAssigneeId, remark); return e; } private async Task TryTransferIntakeOnTransferAsync(long exceptionId, long newAssigneeRecId, string? remark) { try { var instance = await _flowInstanceRep.AsQueryable() .Where(x => x.BizType == "EXCEPTION_REPORT" && x.BizId == exceptionId && x.Status == FlowInstanceStatusEnum.Running) .FirstAsync(); if (instance == null) return; var currentUserId = _userManager.UserId; var task = await _flowTaskRep.AsQueryable() .Where(x => x.InstanceId == instance.Id && x.AssigneeId == currentUserId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); if (task == null) return; // newAssigneeRecId 是 EmployeeMaster.RecID;FlowEngine.Transfer 要 SysUser.UserId。 var targetSysUserId = await _employeeRep.AsQueryable().ClearFilter() .Where(x => x.Id == newAssigneeRecId && x.SysUserId != null) .Select(x => x.SysUserId) .FirstAsync(); if (targetSysUserId == null || targetSysUserId == 0) { _logger.LogWarning( "S8 转派联动审批流跳过:员工 {RecId} 未绑定 SysUser,TB001 任务保留原 assignee", newAssigneeRecId); return; } await _flowEngine.Transfer(task.Id, targetSysUserId.Value, remark ?? "S8 转派(双线合一自动转办)"); } catch (Exception ex) { _logger.LogWarning(ex, "S8 转派时自动转办 TB001 任务失败 exceptionId={Id} newAssigneeRecId={AssigneeId}", exceptionId, newAssigneeRecId); } } public async Task StartProgressAsync(long id, long tenantId, long factoryId, string? remark) { var currentUserId = GetCurrentUserId(); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS")) throw new S8BizException($"状态 {e.Status} 不可开始处理"); var fromStatus = e.Status; e.Status = "IN_PROGRESS"; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "START_PROGRESS", "开始处理", fromStatus, "IN_PROGRESS", currentUserId, null, remark); }, ex => throw ex); // 双线合一:开始处理 = TB001 异常提报审批通过。 // 当前用户必须是 TB001 task 的 AssigneeId,FlowEngine 强校验。 await TryApproveIntakeOnStartProgressAsync(e.Id, currentUserId); return e; } private async Task TryApproveIntakeOnStartProgressAsync(long exceptionId, long currentUserId) { try { var instance = await _flowInstanceRep.AsQueryable() .Where(x => x.BizType == "EXCEPTION_REPORT" && x.BizId == exceptionId && x.Status == FlowInstanceStatusEnum.Running) .FirstAsync(); if (instance == null) return; var task = await _flowTaskRep.AsQueryable() .Where(x => x.InstanceId == instance.Id && x.AssigneeId == currentUserId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); if (task == null) return; await _flowEngine.Approve(task.Id, "S8 已开始处理(双线合一自动同意)"); } catch (Exception ex) { _logger.LogWarning(ex, "S8 开始处理时自动同意 TB001 任务失败 exceptionId={Id} userId={UserId}", exceptionId, currentUserId); } } public async Task UpgradeAsync(long id, long tenantId, long factoryId, string? remark) { var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); if (e.ActiveFlowInstanceId.HasValue) throw new S8BizException("该异常已有进行中的审批流程,请等待审批完成"); if (!S8StatusRules.IsAllowedTransition(e.Status, "ESCALATED")) throw new S8BizException($"状态 {e.Status} 不可升级"); await _flowEngine.StartFlow(new StartFlowInput { BizType = "EXCEPTION_ESCALATION", BizId = e.Id, Title = $"异常升级审批 - {e.ExceptionCode}", Comment = remark, BizData = new Dictionary { ["severity"] = e.Severity, ["sceneCode"] = e.SceneCode, ["priorityLevel"] = e.PriorityLevel, } }); // 状态和时间线由 ExceptionEscalationBizHandler.OnFlowStarted 回调更新 return await LoadAsync(id, tenantId, factoryId) ?? e; } public async Task RejectAsync(long id, long tenantId, long factoryId, string? remark) { var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); if (!S8StatusRules.IsAllowedTransition(e.Status, "REJECTED")) throw new S8BizException($"状态 {e.Status} 不可驳回"); var from = e.Status; e.Status = "REJECTED"; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "REJECT", "驳回", from, "REJECTED", null, null, remark); }, ex => throw ex); // 双线合一:S8 驳回 = TB001 流程整体拒绝(取消所有 pending 任务、Instance 终止)。 await TryRejectIntakeOnRejectAsync(e.Id, remark); return e; } private async Task TryRejectIntakeOnRejectAsync(long exceptionId, string? remark) { try { var instance = await _flowInstanceRep.AsQueryable() .Where(x => x.BizType == "EXCEPTION_REPORT" && x.BizId == exceptionId && x.Status == FlowInstanceStatusEnum.Running) .FirstAsync(); if (instance == null) return; var currentUserId = _userManager.UserId; var task = await _flowTaskRep.AsQueryable() .Where(x => x.InstanceId == instance.Id && x.AssigneeId == currentUserId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); if (task == null) return; await _flowEngine.Reject(task.Id, remark ?? "S8 已驳回(双线合一自动拒绝)"); } catch (Exception ex) { _logger.LogWarning(ex, "S8 驳回时自动拒绝 TB001 流程失败 exceptionId={Id}", exceptionId); } } public async Task SubmitVerificationAsync( long id, long tenantId, long factoryId, long verifierId, string? remark) { var currentUserId = GetCurrentUserId(); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); await EnsureCurrentUserIsOperatorAsync(e.AssigneeId, currentUserId, "只有当前处理人才能提交复检(或当前账号未绑定员工主数据)"); if (verifierId <= 0) throw new S8BizException("请选择检验人"); if (!S8StatusRules.IsAllowedTransition(e.Status, "PENDING_VERIFICATION")) throw new S8BizException($"状态 {e.Status} 不可提交复检"); var from = e.Status; e.Status = "PENDING_VERIFICATION"; e.VerifierId = verifierId; e.VerificationAssignedAt = DateTime.Now; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "VERIFY_SUBMITTED", "提交复检", from, "PENDING_VERIFICATION", currentUserId, null, remark); }, ex => throw ex); // 双线合一:提交复检 = 启动 EXCEPTION_CLOSURE 流程,指派检验人。 // 该流程定义复用,作为复检/关闭确认通用审批载体;handler 只做 ActiveFlowInstanceId 维护。 await TryStartVerificationFlowAsync(e, verifierId, remark); return await LoadAsync(id, tenantId, factoryId) ?? e; } private async Task TryStartVerificationFlowAsync(AdoS8Exception e, long verifierRecId, string? remark) { try { await _flowEngine.StartFlow(new StartFlowInput { BizType = "EXCEPTION_CLOSURE", BizId = e.Id, BizNo = e.ExceptionCode, Title = $"异常复检 - {e.ExceptionCode}", Comment = remark, BizData = new Dictionary { ["sceneCode"] = e.SceneCode ?? string.Empty, ["verifierRecId"] = verifierRecId, } }); } catch (Exception ex) { _logger.LogWarning(ex, "S8 提交复检时启动 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} verifierRecId={VerifierId}", e.Id, verifierRecId); } } public async Task ApproveVerificationAsync( long id, long tenantId, long factoryId, string? remark) { var currentUserId = GetCurrentUserId(); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId, "只有指定检验人才能检验通过(或当前账号未绑定员工主数据)"); if (!S8StatusRules.IsAllowedTransition(e.Status, "CLOSED")) throw new S8BizException($"状态 {e.Status} 不可检验通过"); var from = e.Status; e.Status = "CLOSED"; e.VerifiedAt = DateTime.Now; e.VerificationResult = "APPROVED"; e.VerificationRemark = remark; e.ClosedAt = DateTime.Now; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "VERIFY_APPROVED", "检验通过", from, "CLOSED", currentUserId, null, remark); }, ex => throw ex); // 双线合一:检验通过 = EXCEPTION_CLOSURE 复检流程审批通过。 await TryApproveVerificationFlowAsync(e.Id, currentUserId); return e; } private async Task TryApproveVerificationFlowAsync(long exceptionId, long currentUserId) { try { var instance = await _flowInstanceRep.AsQueryable() .Where(x => x.BizType == "EXCEPTION_CLOSURE" && x.BizId == exceptionId && x.Status == FlowInstanceStatusEnum.Running) .FirstAsync(); if (instance == null) return; var task = await _flowTaskRep.AsQueryable() .Where(x => x.InstanceId == instance.Id && x.AssigneeId == currentUserId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); if (task == null) return; await _flowEngine.Approve(task.Id, "S8 检验通过(双线合一自动同意)"); } catch (Exception ex) { _logger.LogWarning(ex, "S8 检验通过时自动同意 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}", exceptionId, currentUserId); } } public async Task RejectVerificationAsync( long id, long tenantId, long factoryId, string remark) { var currentUserId = GetCurrentUserId(); var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在"); await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId, "只有指定检验人才能检验退回(或当前账号未绑定员工主数据)"); if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS")) throw new S8BizException($"状态 {e.Status} 不可检验退回"); if (string.IsNullOrWhiteSpace(remark)) throw new S8BizException("检验退回必须填写退回原因"); var from = e.Status; e.Status = "IN_PROGRESS"; e.VerifiedAt = DateTime.Now; e.VerificationResult = "REJECTED"; e.VerificationRemark = remark; e.UpdatedAt = DateTime.Now; await _rep.AsTenant().UseTranAsync(async () => { await _rep.UpdateAsync(e); await InsertTimelineAsync(e.Id, "VERIFY_REJECTED", "检验退回", from, "IN_PROGRESS", currentUserId, null, remark); }, ex => throw ex); // 双线合一:检验退回 = EXCEPTION_CLOSURE 复检流程整体拒绝。 await TryRejectVerificationFlowAsync(e.Id, currentUserId, remark); return e; } private async Task TryRejectVerificationFlowAsync(long exceptionId, long currentUserId, string? remark) { try { var instance = await _flowInstanceRep.AsQueryable() .Where(x => x.BizType == "EXCEPTION_CLOSURE" && x.BizId == exceptionId && x.Status == FlowInstanceStatusEnum.Running) .FirstAsync(); if (instance == null) return; var task = await _flowTaskRep.AsQueryable() .Where(x => x.InstanceId == instance.Id && x.AssigneeId == currentUserId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); if (task == null) return; await _flowEngine.Reject(task.Id, remark ?? "S8 检验退回(双线合一自动拒绝)"); } catch (Exception ex) { _logger.LogWarning(ex, "S8 检验退回时自动拒绝 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}", exceptionId, currentUserId); } } public async Task CommentAsync(long id, string? remark) { var e = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted) ?? throw new S8BizException("异常不存在"); await InsertTimelineAsync(e.Id, "COMMENT", "补充说明", e.Status, e.Status, null, null, remark); } private Task LoadAsync(long id, long tenantId, long factoryId) => _rep.GetFirstAsync(x => x.Id == id && x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted); // 统一复用框架登录上下文,避免业务身份继续信任前端传参。 private long GetCurrentUserId() { var currentUserId = _userManager.UserId; if (currentUserId <= 0) throw new S8BizException("未获取到当前登录用户"); return currentUserId; } // 把异常上的处理人/检验人(employeeId) 经 EmployeeMaster.SysUserId 解析到系统账号 ID。 // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位,必须 ClearFilter 跳过多租户全局 filter; // 通过 employee.Id 主键精确查询作为安全边界,无跨租户泄漏风险。 private async Task GetEmployeeSysUserIdAsync(long? employeeId) { if (!employeeId.HasValue || employeeId.Value <= 0) return null; var emp = await _employeeRep.AsQueryable().ClearFilter() .Where(x => x.Id == employeeId.Value) .FirstAsync(); return emp?.SysUserId; } // 鉴权统一入口:要求当前登录用户必须是 employeeId 解析后的 SysUserId。 private async Task EnsureCurrentUserIsOperatorAsync(long? employeeId, long currentUserId, string failMessage) { var ownerSysUserId = await GetEmployeeSysUserIdAsync(employeeId); if (ownerSysUserId != currentUserId) throw new S8BizException(failMessage); } private async Task InsertTimelineAsync(long exceptionId, string code, string label, string? from, string? to, long? operatorId, string? operatorName, string? remark) => await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline { ExceptionId = exceptionId, ActionCode = code, ActionLabel = label, FromStatus = from, ToStatus = to, OperatorId = operatorId, OperatorName = operatorName, ActionRemark = remark, CreatedAt = DateTime.Now }); }