|
|
@@ -0,0 +1,617 @@
|
|
|
+using System.Text.Json;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.ApprovalFlow.Service;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 流程推进引擎 — 核心状态机
|
|
|
+/// 不暴露为 API,由其他 Service 内部调用
|
|
|
+/// </summary>
|
|
|
+public class FlowEngineService : ITransient
|
|
|
+{
|
|
|
+ private readonly SqlSugarRepository<ApprovalFlow> _flowRep;
|
|
|
+ private readonly SqlSugarRepository<ApprovalFlowInstance> _instanceRep;
|
|
|
+ private readonly SqlSugarRepository<ApprovalFlowTask> _taskRep;
|
|
|
+ private readonly SqlSugarRepository<ApprovalFlowLog> _logRep;
|
|
|
+ private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
|
|
|
+ private readonly SqlSugarRepository<SysUser> _userRep;
|
|
|
+ private readonly UserManager _userManager;
|
|
|
+ private readonly FlowNotifyService _notifyService;
|
|
|
+
|
|
|
+ public FlowEngineService(
|
|
|
+ SqlSugarRepository<ApprovalFlow> flowRep,
|
|
|
+ SqlSugarRepository<ApprovalFlowInstance> instanceRep,
|
|
|
+ SqlSugarRepository<ApprovalFlowTask> taskRep,
|
|
|
+ SqlSugarRepository<ApprovalFlowLog> logRep,
|
|
|
+ SqlSugarRepository<SysUserRole> userRoleRep,
|
|
|
+ SqlSugarRepository<SysUser> userRep,
|
|
|
+ UserManager userManager,
|
|
|
+ FlowNotifyService notifyService)
|
|
|
+ {
|
|
|
+ _flowRep = flowRep;
|
|
|
+ _instanceRep = instanceRep;
|
|
|
+ _taskRep = taskRep;
|
|
|
+ _logRep = logRep;
|
|
|
+ _userRoleRep = userRoleRep;
|
|
|
+ _userRep = userRep;
|
|
|
+ _userManager = userManager;
|
|
|
+ _notifyService = notifyService;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+ // 核心生命周期
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 发起流程
|
|
|
+ /// </summary>
|
|
|
+ public async Task<long> StartFlow(StartFlowInput input)
|
|
|
+ {
|
|
|
+ var flow = await _flowRep.AsQueryable()
|
|
|
+ .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete)
|
|
|
+ .OrderByDescending(u => u.Version)
|
|
|
+ .FirstAsync() ?? throw Oops.Oh($"未找到业务类型 [{input.BizType}] 的已发布流程定义");
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(flow.FlowJson))
|
|
|
+ throw Oops.Oh("流程定义的 FlowJson 为空,请先设计流程图");
|
|
|
+
|
|
|
+ var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(flow.FlowJson)
|
|
|
+ ?? throw Oops.Oh("FlowJson 反序列化失败");
|
|
|
+
|
|
|
+ var instance = new ApprovalFlowInstance
|
|
|
+ {
|
|
|
+ FlowId = flow.Id,
|
|
|
+ FlowVersion = flow.Version,
|
|
|
+ BizType = input.BizType,
|
|
|
+ BizId = input.BizId,
|
|
|
+ BizNo = input.BizNo,
|
|
|
+ Title = input.Title ?? $"{flow.Name}-{input.BizNo}",
|
|
|
+ InitiatorId = _userManager.UserId,
|
|
|
+ InitiatorName = _userManager.RealName,
|
|
|
+ Status = FlowInstanceStatusEnum.Running,
|
|
|
+ FlowJsonSnapshot = flow.FlowJson,
|
|
|
+ StartTime = DateTime.Now,
|
|
|
+ };
|
|
|
+
|
|
|
+ var startNode = flowData.Nodes.FirstOrDefault(n =>
|
|
|
+ n.Type is "bpmn:startEvent" or "start-node")
|
|
|
+ ?? throw Oops.Oh("流程图中未找到开始节点");
|
|
|
+
|
|
|
+ var firstTaskNodeId = FindNextNodeId(flowData, startNode.Id);
|
|
|
+ instance.CurrentNodeId = firstTaskNodeId;
|
|
|
+
|
|
|
+ await _instanceRep.InsertAsync(instance);
|
|
|
+
|
|
|
+ await WriteLog(instance.Id, null, startNode.Id, FlowLogActionEnum.Submit, input.Comment);
|
|
|
+
|
|
|
+ await CreateTasksForNode(instance, flowData, firstTaskNodeId);
|
|
|
+
|
|
|
+ await InvokeHandler(input.BizType, h => h.OnFlowStarted(input.BizId, instance.Id));
|
|
|
+
|
|
|
+ return instance.Id;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 同意
|
|
|
+ /// </summary>
|
|
|
+ public async Task Approve(long taskId, string? comment)
|
|
|
+ {
|
|
|
+ var task = await GetPendingTask(taskId);
|
|
|
+ task.Status = FlowTaskStatusEnum.Approved;
|
|
|
+ task.Comment = comment;
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.Approve, comment);
|
|
|
+
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
|
|
|
+ ?? throw Oops.Oh("流程实例不存在");
|
|
|
+
|
|
|
+ if (await IsNodeCompleted(instance, task.NodeId))
|
|
|
+ {
|
|
|
+ await InvokeHandler(instance.BizType,
|
|
|
+ h => h.OnNodeCompleted(instance.BizId, task.NodeId, task.NodeName ?? ""));
|
|
|
+
|
|
|
+ var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
|
|
|
+ await AdvanceToNext(instance, flowData, task.NodeId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 拒绝
|
|
|
+ /// </summary>
|
|
|
+ public async Task Reject(long taskId, string? comment)
|
|
|
+ {
|
|
|
+ var task = await GetPendingTask(taskId);
|
|
|
+ task.Status = FlowTaskStatusEnum.Rejected;
|
|
|
+ task.Comment = comment;
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
|
|
|
+
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
|
|
|
+ ?? throw Oops.Oh("流程实例不存在");
|
|
|
+ instance.Status = FlowInstanceStatusEnum.Rejected;
|
|
|
+ instance.EndTime = DateTime.Now;
|
|
|
+ await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Reject, comment);
|
|
|
+ await InvokeHandler(instance.BizType,
|
|
|
+ h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Rejected));
|
|
|
+ await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id, instance.Title, FlowInstanceStatusEnum.Rejected);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+ // 扩展操作
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 转办
|
|
|
+ /// </summary>
|
|
|
+ public async Task Transfer(long taskId, long targetUserId, string? comment)
|
|
|
+ {
|
|
|
+ var task = await GetPendingTask(taskId);
|
|
|
+ task.Status = FlowTaskStatusEnum.Transferred;
|
|
|
+ task.Comment = comment;
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ task.TransferToId = targetUserId;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ var targetUser = await _userRep.GetByIdAsync(targetUserId);
|
|
|
+ var newTask = new ApprovalFlowTask
|
|
|
+ {
|
|
|
+ InstanceId = task.InstanceId,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ NodeName = task.NodeName,
|
|
|
+ AssigneeId = targetUserId,
|
|
|
+ AssigneeName = targetUser?.RealName,
|
|
|
+ Status = FlowTaskStatusEnum.Pending,
|
|
|
+ };
|
|
|
+ await _taskRep.InsertAsync(newTask);
|
|
|
+
|
|
|
+ await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.Transfer,
|
|
|
+ $"{comment} → 转办给 {targetUser?.RealName}");
|
|
|
+
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
|
|
|
+ await _notifyService.NotifyTransferred(targetUserId, task.InstanceId, instance?.Title ?? "", _userManager.RealName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 撤回(发起人撤回)
|
|
|
+ /// </summary>
|
|
|
+ public async Task Withdraw(long instanceId)
|
|
|
+ {
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(instanceId)
|
|
|
+ ?? throw Oops.Oh("流程实例不存在");
|
|
|
+ if (instance.InitiatorId != _userManager.UserId)
|
|
|
+ throw Oops.Oh("只有发起人可以撤回");
|
|
|
+ if (instance.Status != FlowInstanceStatusEnum.Running)
|
|
|
+ throw Oops.Oh("当前流程状态不允许撤回");
|
|
|
+
|
|
|
+ var pendingTasks = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .ToListAsync();
|
|
|
+ var doneTasks = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instanceId &&
|
|
|
+ t.Status != FlowTaskStatusEnum.Pending &&
|
|
|
+ t.Status != FlowTaskStatusEnum.Cancelled)
|
|
|
+ .CountAsync();
|
|
|
+ if (doneTasks > 0)
|
|
|
+ throw Oops.Oh("已有人审批过,不可撤回");
|
|
|
+
|
|
|
+ var cancelledUserIds = pendingTasks.Select(t => t.AssigneeId).Distinct().ToList();
|
|
|
+
|
|
|
+ foreach (var t in pendingTasks)
|
|
|
+ {
|
|
|
+ t.Status = FlowTaskStatusEnum.Cancelled;
|
|
|
+ t.ActionTime = DateTime.Now;
|
|
|
+ }
|
|
|
+ await _taskRep.AsUpdateable(pendingTasks).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ instance.Status = FlowInstanceStatusEnum.Cancelled;
|
|
|
+ instance.EndTime = DateTime.Now;
|
|
|
+ await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await WriteLog(instanceId, null, instance.CurrentNodeId, FlowLogActionEnum.Withdraw, null);
|
|
|
+ await InvokeHandler(instance.BizType,
|
|
|
+ h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Cancelled));
|
|
|
+
|
|
|
+ await _notifyService.NotifyWithdrawn(cancelledUserIds, instanceId, instance.Title, instance.InitiatorName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 退回上一步
|
|
|
+ /// </summary>
|
|
|
+ public async Task ReturnToPrev(long taskId, string? comment)
|
|
|
+ {
|
|
|
+ var task = await GetPendingTask(taskId);
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId)
|
|
|
+ ?? throw Oops.Oh("流程实例不存在");
|
|
|
+
|
|
|
+ var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
|
|
|
+
|
|
|
+ var prevNodeId = FindPrevUserTaskNodeId(flowData, task.NodeId);
|
|
|
+ if (prevNodeId == null)
|
|
|
+ throw Oops.Oh("已是第一个审批节点,无法退回");
|
|
|
+
|
|
|
+ await CancelPendingTasks(task.InstanceId, task.NodeId);
|
|
|
+ task.Status = FlowTaskStatusEnum.Returned;
|
|
|
+ task.Comment = comment;
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ instance.CurrentNodeId = prevNodeId;
|
|
|
+ await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await CreateTasksForNode(instance, flowData, prevNodeId);
|
|
|
+ await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Return, comment);
|
|
|
+
|
|
|
+ var returnedTasks = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instance.Id && t.NodeId == prevNodeId && t.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .ToListAsync();
|
|
|
+ var returnedUserIds = returnedTasks.Select(t => t.AssigneeId).Distinct().ToList();
|
|
|
+ await _notifyService.NotifyReturned(returnedUserIds, instance.Id, instance.Title, _userManager.RealName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 加签
|
|
|
+ /// </summary>
|
|
|
+ public async Task AddSign(long taskId, long targetUserId, string? comment)
|
|
|
+ {
|
|
|
+ var task = await GetPendingTask(taskId);
|
|
|
+ var targetUser = await _userRep.GetByIdAsync(targetUserId);
|
|
|
+
|
|
|
+ var newTask = new ApprovalFlowTask
|
|
|
+ {
|
|
|
+ InstanceId = task.InstanceId,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ NodeName = task.NodeName,
|
|
|
+ AssigneeId = targetUserId,
|
|
|
+ AssigneeName = targetUser?.RealName,
|
|
|
+ Status = FlowTaskStatusEnum.Pending,
|
|
|
+ IsAddSign = true,
|
|
|
+ AddSignById = _userManager.UserId,
|
|
|
+ };
|
|
|
+ await _taskRep.InsertAsync(newTask);
|
|
|
+
|
|
|
+ await WriteLog(task.InstanceId, taskId, task.NodeId, FlowLogActionEnum.AddSign,
|
|
|
+ $"{comment} → 加签给 {targetUser?.RealName}");
|
|
|
+
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
|
|
|
+ await _notifyService.NotifyAddSign(targetUserId, task.InstanceId, instance?.Title ?? "", _userManager.RealName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 催办
|
|
|
+ /// </summary>
|
|
|
+ public async Task Urge(long instanceId)
|
|
|
+ {
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(instanceId)
|
|
|
+ ?? throw Oops.Oh("流程实例不存在");
|
|
|
+ if (instance.Status != FlowInstanceStatusEnum.Running)
|
|
|
+ throw Oops.Oh("当前流程不在审批中");
|
|
|
+
|
|
|
+ await WriteLog(instanceId, null, instance.CurrentNodeId, FlowLogActionEnum.Urge, "催办");
|
|
|
+
|
|
|
+ var pendingTasks = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instanceId && t.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .ToListAsync();
|
|
|
+ var userIds = pendingTasks.Select(t => t.AssigneeId).Distinct().ToList();
|
|
|
+ await _notifyService.NotifyUrge(userIds, instanceId, instance.Title);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+ // 内部引擎方法
|
|
|
+ // ═══════════════════════════════════════════
|
|
|
+
|
|
|
+ private async Task AdvanceToNext(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string currentNodeId)
|
|
|
+ {
|
|
|
+ var nextNodeId = FindNextNodeId(flowData, currentNodeId);
|
|
|
+ if (nextNodeId == null)
|
|
|
+ {
|
|
|
+ await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var nextNode = flowData.Nodes.FirstOrDefault(n => n.Id == nextNodeId);
|
|
|
+ if (nextNode == null)
|
|
|
+ {
|
|
|
+ await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nextNode.Type is "bpmn:endEvent" or "end-node")
|
|
|
+ {
|
|
|
+ await CompleteInstance(instance, FlowInstanceStatusEnum.Approved);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nextNode.Type is "bpmn:exclusiveGateway")
|
|
|
+ {
|
|
|
+ var bizData = await GetBizData(instance.BizType, instance.BizId);
|
|
|
+ var targetNodeId = EvaluateGateway(nextNode.Properties?.Conditions, flowData, nextNode.Id, bizData);
|
|
|
+ instance.CurrentNodeId = targetNodeId;
|
|
|
+ await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
|
|
|
+ await AdvanceToNext(instance, flowData, nextNode.Id);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ instance.CurrentNodeId = nextNodeId;
|
|
|
+ await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
|
|
|
+ await CreateTasksForNode(instance, flowData, nextNodeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task CompleteInstance(ApprovalFlowInstance instance, FlowInstanceStatusEnum status)
|
|
|
+ {
|
|
|
+ instance.Status = status;
|
|
|
+ instance.EndTime = DateTime.Now;
|
|
|
+ await _instanceRep.AsUpdateable(instance)
|
|
|
+ .UpdateColumns(i => new { i.Status, i.EndTime })
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ await InvokeHandler(instance.BizType,
|
|
|
+ h => h.OnFlowCompleted(instance.BizId, status));
|
|
|
+ await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id, instance.Title, status);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task CreateTasksForNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nodeId)
|
|
|
+ {
|
|
|
+ var node = flowData.Nodes.FirstOrDefault(n => n.Id == nodeId)
|
|
|
+ ?? throw Oops.Oh($"FlowJson 中未找到节点 [{nodeId}]");
|
|
|
+
|
|
|
+ var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId);
|
|
|
+ if (approvers.Count == 0)
|
|
|
+ throw Oops.Oh($"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空");
|
|
|
+
|
|
|
+ var tasks = approvers.Select(a => new ApprovalFlowTask
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ NodeId = nodeId,
|
|
|
+ NodeName = node.Properties?.NodeName ?? node.Text?.Value,
|
|
|
+ AssigneeId = a.userId,
|
|
|
+ AssigneeName = a.userName,
|
|
|
+ Status = FlowTaskStatusEnum.Pending,
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ await _taskRep.AsInsertable(tasks).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ var assigneeIds = tasks.Select(t => t.AssigneeId).Distinct().ToList();
|
|
|
+ await _notifyService.NotifyNewTask(assigneeIds, instance.Id, instance.Title, node.Properties?.NodeName ?? node.Text?.Value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId)
|
|
|
+ {
|
|
|
+ if (props == null || string.IsNullOrWhiteSpace(props.ApproverType))
|
|
|
+ return new List<(long, string)>();
|
|
|
+
|
|
|
+ var approverType = props.ApproverType;
|
|
|
+
|
|
|
+ if (approverType == nameof(ApproverTypeEnum.Initiator))
|
|
|
+ {
|
|
|
+ var initiator = await _userRep.GetByIdAsync(initiatorId);
|
|
|
+ return initiator != null
|
|
|
+ ? new List<(long, string)> { (initiator.Id, initiator.RealName ?? "") }
|
|
|
+ : new List<(long, string)>();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(props.ApproverIds))
|
|
|
+ return new List<(long, string)>();
|
|
|
+
|
|
|
+ var ids = props.ApproverIds.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
|
+ .Select(s => long.TryParse(s.Trim(), out var v) ? v : 0).Where(id => id > 0).ToList();
|
|
|
+
|
|
|
+ if (approverType == nameof(ApproverTypeEnum.SpecificUser))
|
|
|
+ {
|
|
|
+ var users = await _userRep.AsQueryable()
|
|
|
+ .Where(u => ids.Contains(u.Id)).ToListAsync();
|
|
|
+ return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (approverType == nameof(ApproverTypeEnum.Role))
|
|
|
+ {
|
|
|
+ var userIds = await _userRoleRep.AsQueryable()
|
|
|
+ .Where(ur => ids.Contains(ur.RoleId))
|
|
|
+ .Select(ur => ur.UserId)
|
|
|
+ .ToListAsync();
|
|
|
+ var users = await _userRep.AsQueryable()
|
|
|
+ .Where(u => userIds.Contains(u.Id)).ToListAsync();
|
|
|
+ return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (approverType == nameof(ApproverTypeEnum.Department))
|
|
|
+ {
|
|
|
+ var users = await _userRep.AsQueryable()
|
|
|
+ .Where(u => ids.Contains(u.OrgId)).ToListAsync();
|
|
|
+ return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ return new List<(long, string)>();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 评估排他网关 — 依次尝试各非默认分支的条件表达式,首个匹配的获胜;
|
|
|
+ /// 全不匹配则走默认分支;无默认则走第一条出边
|
|
|
+ /// 支持简单比较表达式:variable op value(op: ==,!=,>,>=,<,<=)
|
|
|
+ /// </summary>
|
|
|
+ private string EvaluateGateway(List<GatewayCondition>? conditions, ApprovalFlowItem flowData, string gatewayNodeId, Dictionary<string, object>? bizData)
|
|
|
+ {
|
|
|
+ if (conditions != null && conditions.Count > 0 && bizData != null && bizData.Count > 0)
|
|
|
+ {
|
|
|
+ foreach (var cond in conditions.Where(c => !c.IsDefault))
|
|
|
+ {
|
|
|
+ if (!string.IsNullOrWhiteSpace(cond.Expression) && EvalSimpleExpression(cond.Expression, bizData))
|
|
|
+ return cond.TargetNodeId;
|
|
|
+ }
|
|
|
+
|
|
|
+ var defaultBranch = conditions.FirstOrDefault(c => c.IsDefault);
|
|
|
+ if (defaultBranch != null)
|
|
|
+ return defaultBranch.TargetNodeId;
|
|
|
+ }
|
|
|
+ else if (conditions != null && conditions.Count > 0)
|
|
|
+ {
|
|
|
+ var defaultBranch = conditions.FirstOrDefault(c => c.IsDefault);
|
|
|
+ if (defaultBranch != null) return defaultBranch.TargetNodeId;
|
|
|
+ return conditions.First().TargetNodeId;
|
|
|
+ }
|
|
|
+
|
|
|
+ var edge = flowData.Edges.FirstOrDefault(e => e.SourceNodeId == gatewayNodeId);
|
|
|
+ return edge?.TargetNodeId ?? throw Oops.Oh("排他网关没有出边");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 简单表达式求值:支持 "field op value" 格式(如 "urgent == 1", "customLevel >= 3", "amount > 10000")
|
|
|
+ /// 多条件用 && 连接
|
|
|
+ /// </summary>
|
|
|
+ private static bool EvalSimpleExpression(string expression, Dictionary<string, object> bizData)
|
|
|
+ {
|
|
|
+ var parts = expression.Split("&&", StringSplitOptions.TrimEntries);
|
|
|
+ foreach (var part in parts)
|
|
|
+ {
|
|
|
+ if (!EvalSingleComparison(part.Trim(), bizData))
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool EvalSingleComparison(string expr, Dictionary<string, object> bizData)
|
|
|
+ {
|
|
|
+ string[] ops = { ">=", "<=", "!=", "==", ">", "<" };
|
|
|
+ foreach (var op in ops)
|
|
|
+ {
|
|
|
+ var idx = expr.IndexOf(op, StringComparison.Ordinal);
|
|
|
+ if (idx < 0) continue;
|
|
|
+
|
|
|
+ var fieldName = expr[..idx].Trim();
|
|
|
+ var valueStr = expr[(idx + op.Length)..].Trim().Trim('"', '\'');
|
|
|
+
|
|
|
+ if (!bizData.TryGetValue(fieldName, out var fieldValue))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (decimal.TryParse(fieldValue?.ToString(), out var numLeft) && decimal.TryParse(valueStr, out var numRight))
|
|
|
+ {
|
|
|
+ return op switch
|
|
|
+ {
|
|
|
+ "==" => numLeft == numRight,
|
|
|
+ "!=" => numLeft != numRight,
|
|
|
+ ">" => numLeft > numRight,
|
|
|
+ ">=" => numLeft >= numRight,
|
|
|
+ "<" => numLeft < numRight,
|
|
|
+ "<=" => numLeft <= numRight,
|
|
|
+ _ => false,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ var strLeft = fieldValue?.ToString() ?? "";
|
|
|
+ return op switch
|
|
|
+ {
|
|
|
+ "==" => strLeft.Equals(valueStr, StringComparison.OrdinalIgnoreCase),
|
|
|
+ "!=" => !strLeft.Equals(valueStr, StringComparison.OrdinalIgnoreCase),
|
|
|
+ _ => false,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<Dictionary<string, object>?> GetBizData(string bizType, long bizId)
|
|
|
+ {
|
|
|
+ var handlers = App.GetServices<IFlowBizHandler>();
|
|
|
+ var handler = handlers?.FirstOrDefault(h => h.BizType == bizType);
|
|
|
+ if (handler == null) return null;
|
|
|
+ return await handler.GetBizData(bizId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private string? FindNextNodeId(ApprovalFlowItem flowData, string currentNodeId)
|
|
|
+ {
|
|
|
+ var edge = flowData.Edges.FirstOrDefault(e => e.SourceNodeId == currentNodeId);
|
|
|
+ return edge?.TargetNodeId;
|
|
|
+ }
|
|
|
+
|
|
|
+ private string? FindPrevUserTaskNodeId(ApprovalFlowItem flowData, string currentNodeId)
|
|
|
+ {
|
|
|
+ var inEdge = flowData.Edges.FirstOrDefault(e => e.TargetNodeId == currentNodeId);
|
|
|
+ if (inEdge == null) return null;
|
|
|
+
|
|
|
+ var prevNode = flowData.Nodes.FirstOrDefault(n => n.Id == inEdge.SourceNodeId);
|
|
|
+ if (prevNode == null) return null;
|
|
|
+
|
|
|
+ if (prevNode.Type is "bpmn:userTask" or "user-node" or "task-node")
|
|
|
+ return prevNode.Id;
|
|
|
+
|
|
|
+ // 递归跳过网关等非用户任务节点
|
|
|
+ return FindPrevUserTaskNodeId(flowData, prevNode.Id);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<bool> IsNodeCompleted(ApprovalFlowInstance instance, string nodeId)
|
|
|
+ {
|
|
|
+ var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
|
|
|
+ var node = flowData.Nodes.FirstOrDefault(n => n.Id == nodeId);
|
|
|
+ var mode = node?.Properties?.MultiApproveMode;
|
|
|
+
|
|
|
+ if (mode == nameof(MultiApproveModeEnum.All))
|
|
|
+ {
|
|
|
+ var pendingCount = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instance.Id && t.NodeId == nodeId && t.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .CountAsync();
|
|
|
+ return pendingCount == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认或签(Any):一人通过即完成,取消其他 Pending
|
|
|
+ await CancelPendingTasks(instance.Id, nodeId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task CancelPendingTasks(long instanceId, string nodeId, long? excludeTaskId = null)
|
|
|
+ {
|
|
|
+ var tasks = await _taskRep.AsQueryable()
|
|
|
+ .Where(t => t.InstanceId == instanceId && t.NodeId == nodeId && t.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .WhereIF(excludeTaskId.HasValue, t => t.Id != excludeTaskId!.Value)
|
|
|
+ .ToListAsync();
|
|
|
+ foreach (var t in tasks)
|
|
|
+ {
|
|
|
+ t.Status = FlowTaskStatusEnum.Cancelled;
|
|
|
+ t.ActionTime = DateTime.Now;
|
|
|
+ }
|
|
|
+ if (tasks.Count > 0)
|
|
|
+ await _taskRep.AsUpdateable(tasks).ExecuteCommandAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<ApprovalFlowTask> GetPendingTask(long taskId)
|
|
|
+ {
|
|
|
+ var task = await _taskRep.GetByIdAsync(taskId)
|
|
|
+ ?? throw Oops.Oh("审批任务不存在");
|
|
|
+ if (task.Status != FlowTaskStatusEnum.Pending)
|
|
|
+ throw Oops.Oh("该任务已处理");
|
|
|
+ if (task.AssigneeId != _userManager.UserId)
|
|
|
+ throw Oops.Oh("当前用户不是该任务的审批人");
|
|
|
+ return task;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task WriteLog(long instanceId, long? taskId, string? nodeId, FlowLogActionEnum action, string? comment)
|
|
|
+ {
|
|
|
+ await _logRep.InsertAsync(new ApprovalFlowLog
|
|
|
+ {
|
|
|
+ InstanceId = instanceId,
|
|
|
+ TaskId = taskId,
|
|
|
+ NodeId = nodeId,
|
|
|
+ Action = action,
|
|
|
+ OperatorId = _userManager.UserId,
|
|
|
+ OperatorName = _userManager.RealName,
|
|
|
+ Comment = comment,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ApprovalFlowItem DeserializeFlowJson(string? json)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(json))
|
|
|
+ throw Oops.Oh("FlowJson 快照为空");
|
|
|
+ return JsonSerializer.Deserialize<ApprovalFlowItem>(json)
|
|
|
+ ?? throw Oops.Oh("FlowJson 反序列化失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task InvokeHandler(string bizType, Func<IFlowBizHandler, Task> action)
|
|
|
+ {
|
|
|
+ var handlers = App.GetServices<IFlowBizHandler>();
|
|
|
+ var handler = handlers?.FirstOrDefault(h => h.BizType == bizType);
|
|
|
+ if (handler != null)
|
|
|
+ await action(handler);
|
|
|
+ }
|
|
|
+}
|