namespace Admin.NET.Plugin.ApprovalFlow.Service; /// /// 审批任务服务 /// [ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 90)] public class FlowTaskService : IDynamicApiController, ITransient { private readonly FlowEngineService _engine; private readonly SqlSugarRepository _taskRep; private readonly SqlSugarRepository _instanceRep; private readonly UserManager _userManager; public FlowTaskService( FlowEngineService engine, SqlSugarRepository taskRep, SqlSugarRepository instanceRep, UserManager userManager) { _engine = engine; _taskRep = taskRep; _instanceRep = instanceRep; _userManager = userManager; } // ── 查询 ── /// /// 我的待办(分页) /// [HttpPost] [ApiDescriptionSettings(Name = "MyPendingPage")] [DisplayName("我的待办")] public async Task> MyPendingPage(TaskPageInput input) { var userId = _userManager.UserId; return await _taskRep.AsQueryable() .InnerJoin((t, i) => t.InstanceId == i.Id) .Where((t, i) => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending) .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType) .OrderByDescending((t, i) => t.CreateTime) .Select((t, i) => new TaskPageOutput { Id = t.Id, InstanceId = t.InstanceId, NodeId = t.NodeId, NodeName = t.NodeName, Status = t.Status, Title = i.Title, BizType = i.BizType, BizId = i.BizId, BizNo = i.BizNo, InitiatorName = i.InitiatorName, StartTime = i.StartTime, CreateTime = t.CreateTime, IsDelegate = t.IsDelegate, DelegateForUserName = t.DelegateForUserName, }) .ToPagedListAsync(input.Page, input.PageSize); } /// /// 我的已办(分页) /// [HttpPost] [ApiDescriptionSettings(Name = "MyDonePage")] [DisplayName("我的已办")] public async Task> MyDonePage(TaskPageInput input) { var userId = _userManager.UserId; return await _taskRep.AsQueryable() .InnerJoin((t, i) => t.InstanceId == i.Id) .Where((t, i) => t.AssigneeId == userId && t.Status != FlowTaskStatusEnum.Pending) .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), (t, i) => i.BizType == input.BizType) .OrderByDescending((t, i) => t.ActionTime) .Select((t, i) => new TaskPageOutput { Id = t.Id, InstanceId = t.InstanceId, NodeId = t.NodeId, NodeName = t.NodeName, Status = t.Status, Comment = t.Comment, ActionTime = t.ActionTime, Title = i.Title, BizType = i.BizType, BizId = i.BizId, BizNo = i.BizNo, InitiatorName = i.InitiatorName, StartTime = i.StartTime, CreateTime = t.CreateTime, IsDelegate = t.IsDelegate, DelegateForUserName = t.DelegateForUserName, }) .ToPagedListAsync(input.Page, input.PageSize); } /// /// 我发起的(分页) /// [HttpPost] [ApiDescriptionSettings(Name = "MyInitiatedPage")] [DisplayName("我发起的")] public async Task> MyInitiatedPage(InstancePageInput input) { var userId = _userManager.UserId; var page = await _instanceRep.AsQueryable() .Where(i => i.InitiatorId == userId) .WhereIF(!string.IsNullOrWhiteSpace(input.BizType), i => i.BizType == input.BizType) .WhereIF(input.Status.HasValue, i => i.Status == input.Status!.Value) .OrderByDescending(i => i.CreateTime) .Select(i => new InstancePageOutput { Id = i.Id, BizType = i.BizType, BizId = i.BizId, BizNo = i.BizNo, Title = i.Title, Status = i.Status, CurrentNodeId = i.CurrentNodeId, StartTime = i.StartTime, EndTime = i.EndTime, InitiatorName = i.InitiatorName, }) .ToPagedListAsync(input.Page, input.PageSize); var runningIds = page.Items.Where(x => x.Status == FlowInstanceStatusEnum.Running).Select(x => x.Id).ToList(); if (runningIds.Count > 0) { var pendingAssignees = await _taskRep.AsQueryable() .Where(t => runningIds.Contains(t.InstanceId) && t.Status == FlowTaskStatusEnum.Pending) .Select(t => new { t.InstanceId, t.AssigneeName }) .ToListAsync(); var grouped = pendingAssignees.GroupBy(x => x.InstanceId) .ToDictionary(g => g.Key, g => string.Join(", ", g.Select(x => x.AssigneeName).Distinct())); foreach (var item in page.Items) { if (grouped.TryGetValue(item.Id, out var names)) item.CurrentAssigneeName = names; } } return page; } /// /// 待办数量 /// [HttpGet] [ApiDescriptionSettings(Name = "MyPendingCount")] [DisplayName("待办数量")] public async Task MyPendingCount() { var userId = _userManager.UserId; return await _taskRep.AsQueryable() .Where(t => t.AssigneeId == userId && t.Status == FlowTaskStatusEnum.Pending) .CountAsync(); } // ── 操作 ── /// /// 只能经专用业务入口审批的业务类型 → 对应的引导文案。 /// /// 背景:这些业务在专用服务里额外建立了通用审批链没有的 authority /// (租户解析 → 业务单归属校验 → 当前节点守卫 → UseTranAsync 事务边界), /// 走通用 flowTask/approve|reject 会把这四层全部绕过(引擎侧 GetPendingTask /// 只校验「任务存在 / Pending / 指派人==当前用户」,不校验业务类型与租户)。 /// 仅隐藏前端按钮不足以闭环——手工 HTTP 仍可直达,故在网关层 fail-closed。 /// /// 这里刻意硬编码 BizType 字符串:ApprovalFlow 插件不引用 AiDOP 插件 /// (依赖方向为 AiDOP → ApprovalFlow,反向会成环),无法引用业务侧常量。 /// private static readonly Dictionary DedicatedEntryOnlyBizTypes = new() { ["S5_IQC_INSPBILL"] = "来料检验单请在「来料检验单详情」页完成审核,不支持在审批中心直接同意/拒绝。", // S6 过程检验单:N3_QE_DISPOSITION 的处置记录(ado_s6_ipqc_quality_disposition)是由 // S6ProcessInspectionReview.qe-disposition 在同一事务里 INSERT 后才推进流程的。走通用审批会把 // 流程直接推到 end 而永不写处置记录 —— 流程显示已闭环、处置轨迹却为空,且事后无法补记。 // 另:专用 supervisor-return 要求退回意见必填且回退到 N1,通用 Reject 会把实例直接置为终态。 ["S6_PROCESS_INSPECTION"] = "过程检验单请在「过程检验」业务页完成主管审核或质量处置,不支持在审批中心直接同意/拒绝。", }; /// /// 通用审批动作前置守卫:按 taskId 从库中反查权威 BizType,命中名单即拒绝。 /// /// 只做「拒绝」不做「放行授权」,故不引入新的租户判定 /// (ApprovalFlowTask / ApprovalFlowInstance 无 TenantId 列,租户边界仍由 /// 各业务专用入口的业务表 tenant 谓词负责)。 /// 任务/实例不存在时不在此抛错,交由引擎沿用既有错误语义。 /// private async Task EnsureDedicatedEntryOnlyAsync(long taskId) { var task = await _taskRep.GetByIdAsync(taskId); if (task == null) return; var instance = await _instanceRep.GetByIdAsync(task.InstanceId); if (instance?.BizType == null) return; if (DedicatedEntryOnlyBizTypes.TryGetValue(instance.BizType, out var hint)) throw Oops.Oh(hint); } /// /// 同意 /// [HttpPost] [ApiDescriptionSettings(Name = "Approve")] [DisplayName("同意")] public async Task Approve(TaskActionInput input) { await EnsureDedicatedEntryOnlyAsync(input.TaskId); await _engine.Approve(input.TaskId, input.Comment); } /// /// 拒绝 /// [HttpPost] [ApiDescriptionSettings(Name = "Reject")] [DisplayName("拒绝")] public async Task Reject(TaskActionInput input) { await EnsureDedicatedEntryOnlyAsync(input.TaskId); await _engine.Reject(input.TaskId, input.Comment); } /// /// 转办 /// [HttpPost] [ApiDescriptionSettings(Name = "Transfer")] [DisplayName("转办")] public async Task Transfer(TaskTransferInput input) { await _engine.Transfer(input.TaskId, input.TargetUserId, input.Comment); } /// /// 撤回 /// [HttpPost] [ApiDescriptionSettings(Name = "Withdraw")] [DisplayName("撤回")] public async Task Withdraw(TaskWithdrawInput input) { await _engine.Withdraw(input.InstanceId); } /// /// 退回上一步 /// [HttpPost] [ApiDescriptionSettings(Name = "ReturnToPrev")] [DisplayName("退回上一步")] public async Task ReturnToPrev(TaskActionInput input) { await _engine.ReturnToPrev(input.TaskId, input.Comment); } /// /// 加签 /// [HttpPost] [ApiDescriptionSettings(Name = "AddSign")] [DisplayName("加签")] public async Task AddSign(TaskAddSignInput input) { await _engine.AddSign(input.TaskId, input.TargetUserId, input.Comment); } /// /// 升级 /// [HttpPost] [ApiDescriptionSettings(Name = "Escalate")] [DisplayName("升级")] public async Task Escalate(TaskActionInput input) { await _engine.Escalate(input.TaskId, input.Comment); } /// /// 获取指定任务所属节点的升级配置 /// [HttpGet] [ApiDescriptionSettings(Name = "GetEscalationConfig")] [DisplayName("获取升级配置")] public async Task GetEscalationConfig([FromQuery] long taskId) { var task = await _taskRep.GetByIdAsync(taskId); if (task == null) return new EscalationConfigOutput { Enabled = false }; var instance = await _instanceRep.GetByIdAsync(task.InstanceId); if (instance == null) return new EscalationConfigOutput { Enabled = false }; var flowData = System.Text.Json.JsonSerializer.Deserialize(instance.FlowJsonSnapshot ?? "{}"); var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == task.NodeId); var props = node?.Properties; return new EscalationConfigOutput { Enabled = props?.EnableManualEscalation == true && !string.IsNullOrWhiteSpace(props?.EscalationApproverType) && !string.IsNullOrWhiteSpace(props?.EscalationApproverIds), EscalationApproverNames = props?.EscalationApproverNames, }; } /// /// 催办 /// [HttpPost] [ApiDescriptionSettings(Name = "Urge")] [DisplayName("催办")] public async Task Urge(TaskUrgeInput input) { await _engine.Urge(input.InstanceId); } /// /// 立即触发一次超时任务扫描(等价于 FlowTimeoutJob 的单次执行) /// 仅 SuperAdmin / SysAdmin 可调用,用于运维排障与 E2E 自动化验收。 /// [HttpPost] [ApiDescriptionSettings(Name = "TriggerTimeoutScan")] [DisplayName("立即扫描超时任务")] public async Task TriggerTimeoutScan() { if (!_userManager.SuperAdmin && !_userManager.SysAdmin) throw Oops.Oh("仅超级管理员/系统管理员可触发该操作"); return await _engine.ScanTimeoutTasks(); } /// /// 批量同意 /// [HttpPost] [ApiDescriptionSettings(Name = "BatchApprove")] [DisplayName("批量同意")] public async Task BatchApprove(BatchActionInput input) { var result = new BatchActionResult(); foreach (var taskId in input.TaskIds) { try { await EnsureDedicatedEntryOnlyAsync(taskId); await _engine.Approve(taskId, input.Comment); result.SuccessCount++; } catch (Exception ex) { result.FailCount++; result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message }); } } return result; } /// /// 批量拒绝 /// [HttpPost] [ApiDescriptionSettings(Name = "BatchReject")] [DisplayName("批量拒绝")] public async Task BatchReject(BatchActionInput input) { var result = new BatchActionResult(); foreach (var taskId in input.TaskIds) { try { await EnsureDedicatedEntryOnlyAsync(taskId); await _engine.Reject(taskId, input.Comment); result.SuccessCount++; } catch (Exception ex) { result.FailCount++; result.FailItems.Add(new BatchActionFailItem { TaskId = taskId, Error = ex.Message }); } } return result; } }