|
|
@@ -0,0 +1,927 @@
|
|
|
+# 审批流 — 流程升级机制实施方案
|
|
|
+
|
|
|
+> 文档定位:审批流引擎的通用能力增强——为审批节点增加超时自动处理和手动升级机制
|
|
|
+> 需求来源:S8 异常协同模块的"异常升级"与"关闭确认"场景(见 `doc/S8异常协同-审批流集成功能说明.md`)
|
|
|
+> 创建日期:2026-04-16
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、需求背景
|
|
|
+
|
|
|
+当前审批流引擎的流程设计器已支持在审批节点上配置 `timeoutHours`(超时时长)和 `timeoutAction`(超时动作:Notify / AutoApprove / AutoReject),但**后端没有任何定时扫描和执行逻辑**,这些配置项是空壳。
|
|
|
+
|
|
|
+S8 异常协同的升级场景进一步要求:当审批任务超时或当前审批人无力处理时,能自动/手动将任务升级到更高层级的审批人。这是一个通用能力,不仅 S8 需要,任何业务的审批流都可能用到。
|
|
|
+
|
|
|
+### 本方案目标
|
|
|
+
|
|
|
+在审批流引擎层面新增三项通用能力(不涉及 S8 业务代码改动):
|
|
|
+
|
|
|
+1. **超时自动处理**:让已有的 `timeoutHours` + `timeoutAction` 配置真正生效
|
|
|
+2. **超时自动升级**:新增 `AutoEscalate` 超时动作,超时后自动升级到配置的更高层级审批人
|
|
|
+3. **手动升级**:审批人可主动点击"升级",将任务转交给更高层级审批人
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、现状分析
|
|
|
+
|
|
|
+### 2.1 已有基础设施
|
|
|
+
|
|
|
+| 组件 | 位置 | 状态 |
|
|
|
+|------|------|------|
|
|
|
+| `FlowProperties.TimeoutHours` | `ApprovalFlowItem.cs` L113-115 | 已定义,`int?` |
|
|
|
+| `FlowProperties.TimeoutAction` | `ApprovalFlowItem.cs` L120-122 | 已定义,`string?`(`Notify` / `AutoApprove` / `AutoReject`) |
|
|
|
+| `FlowLogActionEnum.AutoTimeout = 10` | `FlowLogActionEnum.cs` L37 | 已定义 |
|
|
|
+| 设计器 UI:超时小时 + 超时动作 | `PropertyCommon.vue` L118-131 | 已实现(下拉选项仅 3 个) |
|
|
|
+| `FlowEngineService.Approve()` | `FlowEngineService.cs` L113-134 | 引擎核心,可复用其"推进到下一步"逻辑 |
|
|
|
+| 定时任务框架(Furion `IJob`) | `LogJob.cs` 等 | 项目已使用,可照搬模式 |
|
|
|
+
|
|
|
+### 2.2 完全缺失的部分
|
|
|
+
|
|
|
+| 缺失项 | 说明 |
|
|
|
+|--------|------|
|
|
|
+| 超时扫描定时任务 | 没有 `FlowTimeoutJob`,`timeoutHours` 配置无人消费 |
|
|
|
+| `AutoEscalate` 超时动作 | 枚举和 UI 均未包含 |
|
|
|
+| 升级目标配置 | `FlowProperties` 中无 `escalation*` 字段 |
|
|
|
+| 手动升级引擎方法 | `FlowEngineService` 无 `Escalate()` 方法 |
|
|
|
+| 手动升级 API 端点 | `FlowTaskService` 无升级接口 |
|
|
|
+| `Escalated` 任务状态 | `FlowTaskStatusEnum` 无此值 |
|
|
|
+| `Escalate` 日志动作 | `FlowLogActionEnum` 无此值 |
|
|
|
+| 升级通知模板 | `FlowNotifyService` 无升级相关通知 |
|
|
|
+| 前端升级按钮 | `ApprovalPanel.vue` 无"升级"操作 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、总体设计
|
|
|
+
|
|
|
+### 3.1 超时处理流程
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────┐
|
|
|
+│ FlowTimeoutJob(每 5 分钟执行) │
|
|
|
+│ │
|
|
|
+│ 扫描条件: │
|
|
|
+│ task.Status == Pending │
|
|
|
+│ AND 所属节点 timeoutHours > 0 │
|
|
|
+│ AND task.CreateTime + timeoutHours < NOW │
|
|
|
+│ │
|
|
|
+│ 按 timeoutAction 执行: │
|
|
|
+│ ├── Notify → 发催办通知(仅首次) │
|
|
|
+│ ├── AutoApprove → 系统自动同意 │
|
|
|
+│ ├── AutoReject → 系统自动拒绝 │
|
|
|
+│ └── AutoEscalate → 升级到配置的更高层级审批人 │
|
|
|
+└─────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 手动升级流程
|
|
|
+
|
|
|
+```
|
|
|
+审批人 → 点击"升级" → 引擎 Escalate()
|
|
|
+ ├── 当前任务标记为 Escalated
|
|
|
+ ├── 取消同节点其他 Pending 任务
|
|
|
+ ├── 读取节点升级目标配置
|
|
|
+ ├── 为升级目标创建新 Pending 任务(同节点)
|
|
|
+ ├── 写日志 FlowLogActionEnum.Escalate
|
|
|
+ └── 通知升级目标人
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 升级 vs 转办的区别
|
|
|
+
|
|
|
+| 维度 | 转办 (Transfer) | 升级 (Escalate) |
|
|
|
+|------|-----------------|-----------------|
|
|
|
+| 触发者 | 当前审批人 | 当前审批人 或 系统自动 |
|
|
|
+| 目标选择 | 审批人手动选一个用户 | 按节点预配置的升级目标自动路由 |
|
|
|
+| 语义 | 平级移交 | 向上请求更高权限介入 |
|
|
|
+| 任务状态 | `Transferred` | `Escalated`(新增) |
|
|
|
+| 日志动作 | `Transfer` | `Escalate`(新增)或 `AutoTimeout` |
|
|
|
+| 前提 | 始终可用 | 节点必须配置了升级目标 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、详细设计
|
|
|
+
|
|
|
+### 4.1 枚举扩展
|
|
|
+
|
|
|
+#### `FlowTaskStatusEnum.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+[Description("已升级")]
|
|
|
+Escalated = 6,
|
|
|
+```
|
|
|
+
|
|
|
+#### `FlowLogActionEnum.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+[Description("升级")]
|
|
|
+Escalate = 11,
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 节点属性扩展(`FlowProperties`)
|
|
|
+
|
|
|
+在 `ApprovalFlowItem.cs` 的 `FlowProperties` 类中新增升级目标配置字段:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// ── 升级配置 ──
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 升级审批人类型:SpecificUser / Role / Department
|
|
|
+/// 仅当 timeoutAction == "AutoEscalate" 或启用手动升级时有效
|
|
|
+/// </summary>
|
|
|
+[JsonPropertyName("escalationApproverType")]
|
|
|
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
|
+public string? EscalationApproverType { get; set; }
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 升级审批人/角色/部门 Id,逗号分隔
|
|
|
+/// </summary>
|
|
|
+[JsonPropertyName("escalationApproverIds")]
|
|
|
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
|
+public string? EscalationApproverIds { get; set; }
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 升级审批人/角色/部门名称,逗号分隔(冗余,便于前端展示)
|
|
|
+/// </summary>
|
|
|
+[JsonPropertyName("escalationApproverNames")]
|
|
|
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
|
+public string? EscalationApproverNames { get; set; }
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 是否允许手动升级(true 时审批面板显示"升级"按钮)
|
|
|
+/// </summary>
|
|
|
+[JsonPropertyName("enableManualEscalation")]
|
|
|
+[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
|
+public bool? EnableManualEscalation { get; set; }
|
|
|
+```
|
|
|
+
|
|
|
+> 说明:升级目标的解析逻辑复用现有 `ResolveApprovers`,传入 `EscalationApproverType` + `EscalationApproverIds` 即可。
|
|
|
+
|
|
|
+### 4.3 `FlowEngineService` 新增方法
|
|
|
+
|
|
|
+#### 4.3.1 手动升级 `Escalate()`
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 手动升级 — 当前审批人主动将任务升级到更高层级
|
|
|
+/// </summary>
|
|
|
+public async Task Escalate(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 node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId)
|
|
|
+ ?? throw Oops.Oh("节点不存在");
|
|
|
+
|
|
|
+ var props = node.Properties;
|
|
|
+ if (string.IsNullOrWhiteSpace(props?.EscalationApproverType)
|
|
|
+ || string.IsNullOrWhiteSpace(props?.EscalationApproverIds))
|
|
|
+ throw Oops.Oh("该节点未配置升级目标,无法升级");
|
|
|
+
|
|
|
+ // 标记当前任务为已升级
|
|
|
+ task.Status = FlowTaskStatusEnum.Escalated;
|
|
|
+ task.Comment = comment;
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ // 取消同节点其他 Pending 任务
|
|
|
+ await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
|
|
|
+
|
|
|
+ // 解析升级目标并创建新任务
|
|
|
+ var escalationProps = new FlowProperties
|
|
|
+ {
|
|
|
+ ApproverType = props.EscalationApproverType,
|
|
|
+ ApproverIds = props.EscalationApproverIds,
|
|
|
+ ApproverNames = props.EscalationApproverNames,
|
|
|
+ };
|
|
|
+ var escalationApprovers = await ResolveApprovers(escalationProps, instance.InitiatorId);
|
|
|
+ if (escalationApprovers.Count == 0)
|
|
|
+ throw Oops.Oh("升级目标审批人列表为空");
|
|
|
+
|
|
|
+ var newTasks = escalationApprovers.Select(a => new ApprovalFlowTask
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ NodeName = task.NodeName,
|
|
|
+ AssigneeId = a.userId,
|
|
|
+ AssigneeName = a.userName,
|
|
|
+ Status = FlowTaskStatusEnum.Pending,
|
|
|
+ OrgId = instance.OrgId,
|
|
|
+ }).ToList();
|
|
|
+ await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ // 日志
|
|
|
+ var targetNames = string.Join(", ", escalationApprovers.Select(a => a.userName));
|
|
|
+ await WriteLog(instance.Id, taskId, task.NodeId, FlowLogActionEnum.Escalate,
|
|
|
+ $"{comment} → 升级给 {targetNames}");
|
|
|
+
|
|
|
+ // 通知升级目标
|
|
|
+ var targetUserIds = escalationApprovers.Select(a => a.userId).Distinct().ToList();
|
|
|
+ await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
|
|
|
+ _userManager.RealName, task.NodeName);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 4.3.2 系统自动升级 `AutoEscalateTask()`(供定时任务调用)
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 系统自动升级(由定时任务调用,无 UserManager 上下文)
|
|
|
+/// </summary>
|
|
|
+public async Task AutoEscalateTask(ApprovalFlowTask task, FlowProperties nodeProps, ApprovalFlowInstance instance)
|
|
|
+{
|
|
|
+ // 标记当前任务
|
|
|
+ task.Status = FlowTaskStatusEnum.Escalated;
|
|
|
+ task.Comment = "系统自动升级(超时)";
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ // 取消同节点其他 Pending 任务
|
|
|
+ await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
|
|
|
+
|
|
|
+ // 解析升级目标
|
|
|
+ var escalationProps = new FlowProperties
|
|
|
+ {
|
|
|
+ ApproverType = nodeProps.EscalationApproverType,
|
|
|
+ ApproverIds = nodeProps.EscalationApproverIds,
|
|
|
+ };
|
|
|
+ var approvers = await ResolveApprovers(escalationProps, instance.InitiatorId);
|
|
|
+ if (approvers.Count == 0) return; // 无升级目标则静默跳过
|
|
|
+
|
|
|
+ var newTasks = approvers.Select(a => new ApprovalFlowTask
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ NodeName = task.NodeName,
|
|
|
+ AssigneeId = a.userId,
|
|
|
+ AssigneeName = a.userName,
|
|
|
+ Status = FlowTaskStatusEnum.Pending,
|
|
|
+ OrgId = instance.OrgId,
|
|
|
+ }).ToList();
|
|
|
+ await _taskRep.AsInsertable(newTasks).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ // 日志(操作人记为"系统")
|
|
|
+ await _logRep.InsertAsync(new ApprovalFlowLog
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ TaskId = task.Id,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ Action = FlowLogActionEnum.AutoTimeout,
|
|
|
+ OperatorId = 0,
|
|
|
+ OperatorName = "系统",
|
|
|
+ Comment = $"审批超时,自动升级给 {string.Join(", ", approvers.Select(a => a.userName))}",
|
|
|
+ });
|
|
|
+
|
|
|
+ // 通知
|
|
|
+ var targetUserIds = approvers.Select(a => a.userId).Distinct().ToList();
|
|
|
+ await _notifyService.NotifyEscalated(targetUserIds, instance.Id, instance.Title,
|
|
|
+ "系统", task.NodeName);
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 系统自动同意(由定时任务调用)
|
|
|
+/// </summary>
|
|
|
+public async Task AutoApproveTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
|
|
|
+{
|
|
|
+ task.Status = FlowTaskStatusEnum.Approved;
|
|
|
+ task.Comment = "系统自动通过(超时)";
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await _logRep.InsertAsync(new ApprovalFlowLog
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ TaskId = task.Id,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ Action = FlowLogActionEnum.AutoTimeout,
|
|
|
+ OperatorId = 0,
|
|
|
+ OperatorName = "系统",
|
|
|
+ Comment = "审批超时,系统自动通过",
|
|
|
+ });
|
|
|
+
|
|
|
+ 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 AutoRejectTask(ApprovalFlowTask task, ApprovalFlowInstance instance)
|
|
|
+{
|
|
|
+ task.Status = FlowTaskStatusEnum.Rejected;
|
|
|
+ task.Comment = "系统自动拒绝(超时)";
|
|
|
+ task.ActionTime = DateTime.Now;
|
|
|
+ await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
|
|
|
+
|
|
|
+ instance.Status = FlowInstanceStatusEnum.Rejected;
|
|
|
+ instance.EndTime = DateTime.Now;
|
|
|
+ await _instanceRep.AsUpdateable(instance).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ await _logRep.InsertAsync(new ApprovalFlowLog
|
|
|
+ {
|
|
|
+ InstanceId = instance.Id,
|
|
|
+ TaskId = task.Id,
|
|
|
+ NodeId = task.NodeId,
|
|
|
+ Action = FlowLogActionEnum.AutoTimeout,
|
|
|
+ OperatorId = 0,
|
|
|
+ OperatorName = "系统",
|
|
|
+ Comment = "审批超时,系统自动拒绝",
|
|
|
+ });
|
|
|
+
|
|
|
+ await InvokeHandler(instance.BizType,
|
|
|
+ h => h.OnFlowCompleted(instance.BizId, FlowInstanceStatusEnum.Rejected));
|
|
|
+ await _notifyService.NotifyFlowCompleted(instance.InitiatorId, instance.Id,
|
|
|
+ instance.Title, FlowInstanceStatusEnum.Rejected);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **注意**:`AutoApproveTask`、`AutoRejectTask`、`AutoEscalateTask` 在定时任务中调用,此时 `_userManager` 无上下文,因此直接操作 DB 并手动写日志(操作人为"系统"),不走 `GetPendingTask`(其有 userId 校验)。同时需要将 `IsNodeCompleted`、`AdvanceToNext`、`CancelPendingTasks`、`ResolveApprovers` 等方法的访问修饰符从 `private` 改为 `internal`,以便定时任务类可以通过引擎服务调用它们(或者将自动处理逻辑全部封装在 `FlowEngineService` 内部,定时任务仅调用一个 public 方法)。
|
|
|
+
|
|
|
+**推荐封装方式**:在 `FlowEngineService` 中新增一个 public 方法供定时任务调用:
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 处理单个超时任务(由 FlowTimeoutJob 调用)
|
|
|
+/// </summary>
|
|
|
+public async Task HandleTimeoutTask(long taskId)
|
|
|
+{
|
|
|
+ var task = await _taskRep.GetByIdAsync(taskId);
|
|
|
+ if (task == null || task.Status != FlowTaskStatusEnum.Pending) return;
|
|
|
+
|
|
|
+ var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
|
|
|
+ if (instance == null || instance.Status != FlowInstanceStatusEnum.Running) return;
|
|
|
+
|
|
|
+ var flowData = DeserializeFlowJson(instance.FlowJsonSnapshot);
|
|
|
+ var node = flowData.Nodes.FirstOrDefault(n => n.Id == task.NodeId);
|
|
|
+ var props = node?.Properties;
|
|
|
+ if (props == null) return;
|
|
|
+
|
|
|
+ var action = props.TimeoutAction;
|
|
|
+ switch (action)
|
|
|
+ {
|
|
|
+ case "Notify":
|
|
|
+ await _notifyService.NotifyUrge(new List<long> { task.AssigneeId },
|
|
|
+ instance.Id, instance.Title);
|
|
|
+ break;
|
|
|
+ case "AutoApprove":
|
|
|
+ await AutoApproveTask(task, instance);
|
|
|
+ break;
|
|
|
+ case "AutoReject":
|
|
|
+ await AutoRejectTask(task, instance);
|
|
|
+ break;
|
|
|
+ case "AutoEscalate":
|
|
|
+ await AutoEscalateTask(task, props, instance);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 超时扫描定时任务 `FlowTimeoutJob`
|
|
|
+
|
|
|
+**新建文件**:`server/Plugins/Admin.NET.Plugin.ApprovalFlow/Job/FlowTimeoutJob.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Text.Json;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.ApprovalFlow;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 审批超时自动处理作业 — 每 5 分钟扫描一次超时的待办任务
|
|
|
+/// </summary>
|
|
|
+[JobDetail("job_flow_timeout", Description = "审批超时自动处理",
|
|
|
+ GroupName = "default", Concurrent = false)]
|
|
|
+[Period(300000, TriggerId = "trigger_flow_timeout", Description = "每5分钟执行")]
|
|
|
+public class FlowTimeoutJob : IJob
|
|
|
+{
|
|
|
+ private readonly IServiceScopeFactory _scopeFactory;
|
|
|
+ private readonly ILogger _logger;
|
|
|
+
|
|
|
+ public FlowTimeoutJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
|
|
|
+ {
|
|
|
+ _scopeFactory = scopeFactory;
|
|
|
+ _logger = loggerFactory.CreateLogger("FlowTimeoutJob");
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
|
|
|
+ {
|
|
|
+ using var scope = _scopeFactory.CreateScope();
|
|
|
+ var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
|
|
+ var engine = scope.ServiceProvider.GetRequiredService<FlowEngineService>();
|
|
|
+
|
|
|
+ // 1. 查询所有 Running 实例的 Pending 任务
|
|
|
+ var pendingTasks = await db.Queryable<ApprovalFlowTask>()
|
|
|
+ .InnerJoin<ApprovalFlowInstance>((t, i) => t.InstanceId == i.Id)
|
|
|
+ .Where((t, i) => t.Status == FlowTaskStatusEnum.Pending
|
|
|
+ && i.Status == FlowInstanceStatusEnum.Running)
|
|
|
+ .Select((t, i) => new
|
|
|
+ {
|
|
|
+ Task = t,
|
|
|
+ FlowJsonSnapshot = i.FlowJsonSnapshot,
|
|
|
+ })
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var processed = 0;
|
|
|
+
|
|
|
+ foreach (var item in pendingTasks)
|
|
|
+ {
|
|
|
+ if (stoppingToken.IsCancellationRequested) break;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(item.FlowJsonSnapshot)) continue;
|
|
|
+
|
|
|
+ var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(item.FlowJsonSnapshot);
|
|
|
+ var node = flowData?.Nodes?.FirstOrDefault(n => n.Id == item.Task.NodeId);
|
|
|
+ var props = node?.Properties;
|
|
|
+
|
|
|
+ if (props?.TimeoutHours == null || props.TimeoutHours <= 0) continue;
|
|
|
+ if (string.IsNullOrWhiteSpace(props.TimeoutAction)) continue;
|
|
|
+
|
|
|
+ // 计算超时:任务创建时间 + 超时小时数
|
|
|
+ var deadline = item.Task.CreateTime.AddHours(props.TimeoutHours.Value);
|
|
|
+ if (now < deadline) continue;
|
|
|
+
|
|
|
+ // 对 Notify 动作:避免重复通知(检查是否已有 AutoTimeout 日志)
|
|
|
+ if (props.TimeoutAction == "Notify")
|
|
|
+ {
|
|
|
+ var alreadyNotified = await db.Queryable<ApprovalFlowLog>()
|
|
|
+ .AnyAsync(log => log.TaskId == item.Task.Id
|
|
|
+ && log.Action == FlowLogActionEnum.AutoTimeout);
|
|
|
+ if (alreadyNotified) continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ await engine.HandleTimeoutTask(item.Task.Id);
|
|
|
+ processed++;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "处理超时任务 {TaskId} 失败", item.Task.Id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (processed > 0)
|
|
|
+ _logger.LogInformation("FlowTimeoutJob 本轮处理了 {Count} 个超时任务", processed);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.5 API 端点扩展(`FlowTaskService`)
|
|
|
+
|
|
|
+新增升级接口:
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 升级
|
|
|
+/// </summary>
|
|
|
+[HttpPost]
|
|
|
+[ApiDescriptionSettings(Name = "Escalate")]
|
|
|
+[DisplayName("升级")]
|
|
|
+public async Task Escalate(TaskActionInput input)
|
|
|
+{
|
|
|
+ await _engine.Escalate(input.TaskId, input.Comment);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+新增查询接口(供前端判断节点是否配置了升级目标):
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 获取指定任务所属节点的升级配置
|
|
|
+/// </summary>
|
|
|
+[HttpGet]
|
|
|
+[ApiDescriptionSettings(Name = "GetEscalationConfig")]
|
|
|
+[DisplayName("获取升级配置")]
|
|
|
+public async Task<EscalationConfigOutput> 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 = JsonSerializer.Deserialize<ApprovalFlowItem>(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,
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+DTO 新增:
|
|
|
+
|
|
|
+```csharp
|
|
|
+public class EscalationConfigOutput
|
|
|
+{
|
|
|
+ public bool Enabled { get; set; }
|
|
|
+ public string? EscalationApproverNames { get; set; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.6 通知扩展(`FlowNotifyService`)
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 升级通知 — 通知升级目标人
|
|
|
+/// </summary>
|
|
|
+public async Task NotifyEscalated(List<long> targetUserIds, long instanceId,
|
|
|
+ string title, string? fromName, string? nodeName)
|
|
|
+{
|
|
|
+ await NotifyUsers(targetUserIds, new FlowNotification
|
|
|
+ {
|
|
|
+ Type = FlowNotificationTypeEnum.Escalated,
|
|
|
+ InstanceId = instanceId,
|
|
|
+ Title = $"【升级】{title}",
|
|
|
+ Content = $"{fromName} 将审批任务({nodeName})升级给您,请及时处理。",
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 超时提醒通知 — 通知当前审批人任务即将/已超时
|
|
|
+/// </summary>
|
|
|
+public async Task NotifyTimeout(List<long> userIds, long instanceId, string title)
|
|
|
+{
|
|
|
+ await NotifyUsers(userIds, new FlowNotification
|
|
|
+ {
|
|
|
+ Type = FlowNotificationTypeEnum.Timeout,
|
|
|
+ InstanceId = instanceId,
|
|
|
+ Title = $"【超时提醒】{title}",
|
|
|
+ Content = "您有一条审批任务已超时,请尽快处理。",
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`FlowNotificationTypeEnum` 新增:
|
|
|
+
|
|
|
+```csharp
|
|
|
+Escalated,
|
|
|
+Timeout,
|
|
|
+```
|
|
|
+
|
|
|
+### 4.7 前端设计器扩展(`PropertyCommon.vue`)
|
|
|
+
|
|
|
+在"审批配置" tab 中新增以下控件:
|
|
|
+
|
|
|
+**a. 超时动作增加"自动升级"选项**
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- 原有 3 个选项后增加 -->
|
|
|
+<el-option label="自动升级" value="AutoEscalate" />
|
|
|
+```
|
|
|
+
|
|
|
+**b. 升级目标配置区域**
|
|
|
+
|
|
|
+当 `timeoutAction == 'AutoEscalate'` 或 `enableManualEscalation == true` 时显示:
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-col :span="24" class="mb20">
|
|
|
+ <el-form-item label="允许手动升级">
|
|
|
+ <el-switch v-model="formData.enableManualEscalation" />
|
|
|
+ </el-form-item>
|
|
|
+</el-col>
|
|
|
+
|
|
|
+<!-- 升级目标配置(当需要升级时显示) -->
|
|
|
+<template v-if="formData.timeoutAction === 'AutoEscalate' || formData.enableManualEscalation">
|
|
|
+ <el-divider content-position="left">升级目标配置</el-divider>
|
|
|
+ <el-col :span="24" class="mb20">
|
|
|
+ <el-form-item label="升级目标类型">
|
|
|
+ <el-select v-model="formData.escalationApproverType" placeholder="请选择">
|
|
|
+ <el-option label="指定用户" value="SpecificUser" />
|
|
|
+ <el-option label="指定角色" value="Role" />
|
|
|
+ <el-option label="指定部门" value="Department" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <!-- 复用现有的用户搜索/角色下拉/部门树组件,绑定到 escalation* 字段 -->
|
|
|
+ <!-- ... 与上方审批人选择逻辑相同,字段改为 escalationApproverIds/Names -->
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+**c. `formData` 和 `saveProperties` 扩展**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// formData 新增
|
|
|
+escalationApproverType: '',
|
|
|
+escalationApproverIds: '',
|
|
|
+escalationApproverNames: '',
|
|
|
+enableManualEscalation: false,
|
|
|
+
|
|
|
+// saveProperties 中新增
|
|
|
+if (formData.timeoutAction === 'AutoEscalate' || formData.enableManualEscalation) {
|
|
|
+ data.escalationApproverType = formData.escalationApproverType;
|
|
|
+ data.escalationApproverIds = formData.escalationApproverIds;
|
|
|
+ data.escalationApproverNames = formData.escalationApproverNames;
|
|
|
+ data.enableManualEscalation = formData.enableManualEscalation;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.8 前端审批面板扩展(`ApprovalPanel.vue`)
|
|
|
+
|
|
|
+新增"升级"按钮,仅在节点启用了手动升级时显示:
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- 在现有操作按钮区域新增 -->
|
|
|
+<el-button
|
|
|
+ v-if="escalationConfig.enabled"
|
|
|
+ type="warning"
|
|
|
+ @click="showEscalateDialog = true"
|
|
|
+>
|
|
|
+ 升级
|
|
|
+</el-button>
|
|
|
+```
|
|
|
+
|
|
|
+逻辑:
|
|
|
+1. 组件 mount 时,调用 `GET /api/flowTask/getEscalationConfig?taskId=xxx` 获取当前任务的升级配置
|
|
|
+2. 如果 `enabled == true`,显示"升级"按钮
|
|
|
+3. 点击后弹出确认弹窗(含升级原因 textarea)
|
|
|
+4. 确认后调用 `POST /api/flowTask/escalate`,传 `taskId` + `comment`
|
|
|
+5. 操作完成后触发 `refresh` 事件
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、改动文件清单
|
|
|
+
|
|
|
+| # | 文件 | 改动类型 | 说明 |
|
|
|
+|---|------|----------|------|
|
|
|
+| 1 | `Enum/FlowTaskStatusEnum.cs` | 修改 | 新增 `Escalated = 6` |
|
|
|
+| 2 | `Enum/FlowLogActionEnum.cs` | 修改 | 新增 `Escalate = 11` |
|
|
|
+| 3 | `Service/ApprovalFlow/Dto/ApprovalFlowItem.cs` | 修改 | `FlowProperties` 新增 4 个升级配置字段 |
|
|
|
+| 4 | `Service/FlowEngine/FlowEngineService.cs` | 修改 | 新增 `Escalate()`、`HandleTimeoutTask()`、`AutoEscalateTask()`、`AutoApproveTask()`、`AutoRejectTask()` |
|
|
|
+| 5 | `Service/FlowTask/FlowTaskService.cs` | 修改 | 新增 `Escalate`、`GetEscalationConfig` API |
|
|
|
+| 6 | `Service/FlowTask/Dto/FlowTaskDtos.cs` | 修改 | 新增 `EscalationConfigOutput` DTO |
|
|
|
+| 7 | `Service/FlowNotify/FlowNotifyService.cs` | 修改 | 新增 `NotifyEscalated`、`NotifyTimeout`;枚举新增 `Escalated`、`Timeout` |
|
|
|
+| 8 | **新建** `Job/FlowTimeoutJob.cs` | 新建 | 超时扫描定时任务 |
|
|
|
+| 9 | `Web/.../PropertyCommon.vue` | 修改 | 超时动作增加"自动升级";新增升级目标配置 UI |
|
|
|
+| 10 | `Web/.../ApprovalPanel.vue` | 修改 | 新增"升级"按钮和确认弹窗 |
|
|
|
+
|
|
|
+### 不改的部分
|
|
|
+
|
|
|
+- `IFlowBizHandler` 接口签名不变
|
|
|
+- 现有审批/拒绝/转办/退回/加签/催办/撤回逻辑不变
|
|
|
+- S8 业务代码不动
|
|
|
+- 数据库表结构不变(`FlowProperties` 存在 FlowJson 中,无需 ALTER TABLE)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、验收标准
|
|
|
+
|
|
|
+### 6.1 超时自动处理(新增功能)
|
|
|
+
|
|
|
+- [ ] 配置超时 1 小时 + `Notify`:超时后审批人收到催办通知,且仅发一次
|
|
|
+- [ ] 配置超时 1 小时 + `AutoApprove`:超时后任务自动通过,流程正常推进到下一节点
|
|
|
+- [ ] 配置超时 1 小时 + `AutoReject`:超时后任务自动拒绝,流程结束,发起人收到拒绝通知
|
|
|
+- [ ] 配置超时 1 小时 + `AutoEscalate`:超时后任务自动升级到配置的更高层级审批人,升级目标收到新待办
|
|
|
+- [ ] 未配置超时的节点不受定时任务影响
|
|
|
+- [ ] 定时任务异常时不影响其他任务的扫描处理(单任务隔离)
|
|
|
+
|
|
|
+### 6.2 手动升级(新增功能)
|
|
|
+
|
|
|
+- [ ] 节点开启 `enableManualEscalation` 且配置了升级目标后,审批面板显示"升级"按钮
|
|
|
+- [ ] 节点未配置升级目标时,"升级"按钮不显示
|
|
|
+- [ ] 点击"升级"后,原任务标记 `Escalated`,同节点其他 Pending 任务被取消
|
|
|
+- [ ] 升级目标收到新的待办任务和通知
|
|
|
+- [ ] 升级后审批时间线正确记录升级事件(操作人、升级目标、升级原因)
|
|
|
+- [ ] 升级目标审批通过后,流程正常推进到下一节点
|
|
|
+- [ ] 升级目标审批拒绝后,流程正常终止
|
|
|
+
|
|
|
+### 6.3 设计器(新增功能)
|
|
|
+
|
|
|
+- [ ] 超时动作下拉新增"自动升级"选项
|
|
|
+- [ ] 新增"允许手动升级"开关
|
|
|
+- [ ] 选择"自动升级"或开启"允许手动升级"后,出现升级目标配置区域
|
|
|
+- [ ] 升级目标支持选择用户/角色/部门,与审批人配置体验一致
|
|
|
+- [ ] 配置保存后 FlowJson 中正确包含 `escalation*` 字段
|
|
|
+- [ ] 重新打开节点属性时,升级配置正确回显
|
|
|
+
|
|
|
+### 6.4 既有功能全量回归测试
|
|
|
+
|
|
|
+以下为审批流引擎现有功能的全量回归,确保本次改动不引入破坏。
|
|
|
+
|
|
|
+#### 6.4.1 流程发起
|
|
|
+
|
|
|
+- [ ] 正常发起审批流程,实例状态为 `Running`,第一个审批节点生成待办任务
|
|
|
+- [ ] 发起时业务回调 `OnFlowStarted` 正确触发
|
|
|
+- [ ] 未发布流程定义时,发起报错提示"未找到已发布流程定义"
|
|
|
+
|
|
|
+#### 6.4.2 同意(Approve)
|
|
|
+
|
|
|
+- [ ] 单人审批:同意后任务状态 `Approved`,流程推进到下一节点
|
|
|
+- [ ] 或签(Any):一人同意后其他 Pending 任务自动取消,流程推进
|
|
|
+- [ ] 会签(All):需所有人同意后流程才推进,部分同意时流程停留在当前节点
|
|
|
+- [ ] 最终节点同意后,实例状态 `Approved`,回调 `OnFlowCompleted(Approved)` 触发
|
|
|
+- [ ] 同意后 `OnNodeCompleted` 回调正确触发
|
|
|
+
|
|
|
+#### 6.4.3 拒绝(Reject)
|
|
|
+
|
|
|
+- [ ] 拒绝后任务状态 `Rejected`,同节点其他 Pending 任务取消
|
|
|
+- [ ] 实例状态 `Rejected`,回调 `OnFlowCompleted(Rejected)` 触发
|
|
|
+- [ ] 发起人收到拒绝通知
|
|
|
+
|
|
|
+#### 6.4.4 转办(Transfer)
|
|
|
+
|
|
|
+- [ ] 转办后原任务 `Transferred`,新任务 `Pending` 分配给目标用户
|
|
|
+- [ ] 目标用户收到转办通知
|
|
|
+- [ ] 目标用户可正常审批(同意/拒绝)
|
|
|
+
|
|
|
+#### 6.4.5 撤回(Withdraw)
|
|
|
+
|
|
|
+- [ ] 发起人可撤回 Running 状态的流程(仅首节点无人审批时)
|
|
|
+- [ ] 撤回后所有 Pending 任务取消,实例状态 `Cancelled`
|
|
|
+- [ ] 回调 `OnFlowCompleted(Cancelled)` 触发
|
|
|
+- [ ] 已有人审批过的流程不允许撤回
|
|
|
+
|
|
|
+#### 6.4.6 退回上一步(ReturnToPrev)
|
|
|
+
|
|
|
+- [ ] 退回后当前节点任务取消,上一个用户任务节点重新生成待办
|
|
|
+- [ ] 实例 `CurrentNodeId` 更新为上一节点
|
|
|
+- [ ] 被退回的审批人收到退回通知
|
|
|
+- [ ] 第一个审批节点无法退回(提示"已是第一个审批节点")
|
|
|
+
|
|
|
+#### 6.4.7 加签(AddSign)
|
|
|
+
|
|
|
+- [ ] 加签后新增一个 Pending 任务给目标用户,标记 `IsAddSign=true`
|
|
|
+- [ ] 原审批人的任务保持 Pending
|
|
|
+- [ ] 加签目标收到通知
|
|
|
+
|
|
|
+#### 6.4.8 催办(Urge)
|
|
|
+
|
|
|
+- [ ] 催办后所有当前节点 Pending 审批人收到催办通知
|
|
|
+- [ ] 时间线记录催办操作
|
|
|
+
|
|
|
+#### 6.4.9 条件网关(Exclusive Gateway)
|
|
|
+
|
|
|
+- [ ] 网关根据 `GetBizData` 返回的业务变量正确路由
|
|
|
+- [ ] 多条件匹配时走第一个命中的分支
|
|
|
+- [ ] 所有条件不匹配时走 `default` 分支
|
|
|
+- [ ] 无 `default` 分支且不匹配时走第一条出边
|
|
|
+
|
|
|
+#### 6.4.10 通知
|
|
|
+
|
|
|
+- [ ] 新任务通知、同意/拒绝通知、转办/退回/加签/催办/撤回通知均正常发送
|
|
|
+- [ ] SignalR 站内消息正常推送
|
|
|
+
|
|
|
+#### 6.4.11 审批中心页面
|
|
|
+
|
|
|
+- [ ] "我的待办"列表正确显示当前用户的 Pending 任务
|
|
|
+- [ ] "我的已办"列表正确显示已处理任务
|
|
|
+- [ ] "我发起的"列表正确显示发起人的流程实例及当前审批人
|
|
|
+- [ ] 待办数量接口返回正确
|
|
|
+
|
|
|
+#### 6.4.12 审批面板(ApprovalPanel)
|
|
|
+
|
|
|
+- [ ] 无实例时显示"提交审批"按钮,点击后发起流程
|
|
|
+- [ ] 有实例且当前用户有 Pending 任务时,显示同意/拒绝/转办/退回/加签按钮
|
|
|
+- [ ] 发起人在 Running 状态时可催办和撤回
|
|
|
+- [ ] 审批时间线正确展示所有操作记录
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、实施步骤
|
|
|
+
|
|
|
+### Step 1:后端枚举与 DTO 扩展
|
|
|
+
|
|
|
+**目标**:为升级机制准备数据模型基础
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 1.1 | `FlowTaskStatusEnum` 新增 `Escalated = 6` | `Enum/FlowTaskStatusEnum.cs` |
|
|
|
+| 1.2 | `FlowLogActionEnum` 新增 `Escalate = 11` | `Enum/FlowLogActionEnum.cs` |
|
|
|
+| 1.3 | `FlowProperties` 新增 `EscalationApproverType`、`EscalationApproverIds`、`EscalationApproverNames`、`EnableManualEscalation` 四个字段 | `Service/ApprovalFlow/Dto/ApprovalFlowItem.cs` |
|
|
|
+| 1.4 | 新增 `EscalationConfigOutput` DTO | `Service/FlowTask/Dto/FlowTaskDtos.cs` |
|
|
|
+| 1.5 | `FlowNotificationTypeEnum` 新增 `Escalated`、`Timeout` | `Service/FlowNotify/FlowNotifyService.cs` |
|
|
|
+
|
|
|
+**完成标志**:后端编译通过,无报错
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 2:引擎核心逻辑——手动升级
|
|
|
+
|
|
|
+**目标**:实现审批人主动升级操作
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 2.1 | 新增 `Escalate(long taskId, string? comment)` 方法 | `FlowEngineService.cs` |
|
|
|
+| 2.2 | 逻辑:校验节点升级配置 → 标记当前任务 `Escalated` → 取消同节点其他 Pending → 解析升级目标 → 创建新 Pending 任务 → 写日志 → 发通知 | `FlowEngineService.cs` |
|
|
|
+| 2.3 | `FlowNotifyService` 新增 `NotifyEscalated()` 方法 | `FlowNotifyService.cs` |
|
|
|
+| 2.4 | `FlowTaskService` 新增 `Escalate` API 端点 | `FlowTaskService.cs` |
|
|
|
+| 2.5 | `FlowTaskService` 新增 `GetEscalationConfig` 查询端点 | `FlowTaskService.cs` |
|
|
|
+
|
|
|
+**完成标志**:通过 API 调用手动升级,升级目标收到新待办任务,时间线记录正确
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 3:引擎核心逻辑——超时自动处理
|
|
|
+
|
|
|
+**目标**:实现超时自动通知/通过/拒绝/升级
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 3.1 | 新增 `HandleTimeoutTask(long taskId)` 统一入口方法 | `FlowEngineService.cs` |
|
|
|
+| 3.2 | 新增 `AutoApproveTask()`:系统自动同意,推进流程 | `FlowEngineService.cs` |
|
|
|
+| 3.3 | 新增 `AutoRejectTask()`:系统自动拒绝,终止流程 | `FlowEngineService.cs` |
|
|
|
+| 3.4 | 新增 `AutoEscalateTask()`:系统自动升级到配置目标 | `FlowEngineService.cs` |
|
|
|
+| 3.5 | `FlowNotifyService` 新增 `NotifyTimeout()` 方法 | `FlowNotifyService.cs` |
|
|
|
+
|
|
|
+**注意**:以上方法在定时任务中调用,无 `UserManager` 上下文,操作人记为"系统",不走 `GetPendingTask` 的用户校验
|
|
|
+
|
|
|
+**完成标志**:直接通过代码调用 `HandleTimeoutTask` 可执行 4 种超时动作,各自行为正确
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 4:超时扫描定时任务
|
|
|
+
|
|
|
+**目标**:让超时处理自动运行
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 4.1 | 新建 `FlowTimeoutJob`:每 5 分钟扫描超时的 Pending 任务 | **新建** `Job/FlowTimeoutJob.cs` |
|
|
|
+| 4.2 | 扫描逻辑:查所有 Running 实例的 Pending 任务 → 取节点 `timeoutHours` → 判断是否超时 → 调用 `HandleTimeoutTask` | `Job/FlowTimeoutJob.cs` |
|
|
|
+| 4.3 | `Notify` 动作防重复:检查是否已有 `AutoTimeout` 日志,有则跳过 | `Job/FlowTimeoutJob.cs` |
|
|
|
+| 4.4 | 单任务异常隔离:`try-catch` 包裹每个任务处理,不影响其他任务 | `Job/FlowTimeoutJob.cs` |
|
|
|
+
|
|
|
+**完成标志**:启动后端,在数据库中创建一个配置了超时的审批任务,等待 5 分钟后定时任务自动触发对应动作
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 5:前端设计器扩展
|
|
|
+
|
|
|
+**目标**:管理员可在流程设计器中配置升级目标
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 5.1 | 超时动作下拉增加"自动升级"(`AutoEscalate`)选项 | `PropertyCommon.vue` |
|
|
|
+| 5.2 | 新增"允许手动升级"开关(`enableManualEscalation`) | `PropertyCommon.vue` |
|
|
|
+| 5.3 | 当需要升级时(自动升级或手动升级启用),显示升级目标配置区域:类型选择 + 用户搜索/角色下拉/部门树 | `PropertyCommon.vue` |
|
|
|
+| 5.4 | `formData` 新增 `escalationApproverType`、`escalationApproverIds`、`escalationApproverNames`、`enableManualEscalation` | `PropertyCommon.vue` |
|
|
|
+| 5.5 | `saveProperties` 将升级配置写入节点 JSON | `PropertyCommon.vue` |
|
|
|
+| 5.6 | `watch(nodeData)` 中回显升级配置 | `PropertyCommon.vue` |
|
|
|
+
|
|
|
+**完成标志**:在设计器中配置升级目标,保存后重新打开节点属性,配置正确回显;发布流程后 FlowJson 包含 `escalation*` 字段
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 6:前端审批面板扩展
|
|
|
+
|
|
|
+**目标**:审批人可在审批面板中手动升级
|
|
|
+
|
|
|
+| 序号 | 任务 | 文件 |
|
|
|
+|------|------|------|
|
|
|
+| 6.1 | 组件加载时调用 `GetEscalationConfig` 获取当前任务的升级配置 | `ApprovalPanel.vue` |
|
|
|
+| 6.2 | 当 `enabled == true` 时显示"升级"按钮(`type="warning"`) | `ApprovalPanel.vue` |
|
|
|
+| 6.3 | 点击"升级"弹出确认弹窗:显示升级目标名称 + 升级原因 textarea | `ApprovalPanel.vue` |
|
|
|
+| 6.4 | 确认后调用 `POST /api/flowTask/escalate`,完成后触发 `refresh` | `ApprovalPanel.vue` |
|
|
|
+
|
|
|
+**完成标志**:在启用了手动升级的节点,审批面板显示"升级"按钮;点击后任务升级成功,面板刷新
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 7:编译与后端启动验证
|
|
|
+
|
|
|
+| 序号 | 任务 |
|
|
|
+|------|------|
|
|
|
+| 7.1 | 后端 `dotnet build` 编译通过,无错误无警告 |
|
|
|
+| 7.2 | 后端 `dotnet run` 启动正常,定时任务注册成功(日志中可见 `job_flow_timeout`) |
|
|
|
+| 7.3 | 前端 `pnpm dev` 启动正常,设计器和审批面板页面无报错 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Step 8:全量测试
|
|
|
+
|
|
|
+按照第六章"验收标准"逐项执行测试:
|
|
|
+
|
|
|
+| 序号 | 测试范围 | 对应验收章节 |
|
|
|
+|------|----------|------------|
|
|
|
+| 8.1 | 超时自动处理:4 种超时动作各一条 | 6.1 |
|
|
|
+| 8.2 | 手动升级:配置/未配置升级目标、升级后审批通过/拒绝 | 6.2 |
|
|
|
+| 8.3 | 设计器:配置保存、回显、FlowJson 内容 | 6.3 |
|
|
|
+| 8.4 | 全量回归:发起、同意、拒绝、转办、撤回、退回、加签、催办、网关、通知、审批中心、审批面板 | 6.4.1 ~ 6.4.12 |
|
|
|
+
|
|
|
+**完成标志**:验收标准全部 checkbox 打勾
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、工作量估算
|
|
|
+
|
|
|
+| 阶段 | 对应 Step | 内容 | 预计工时 |
|
|
|
+|------|-----------|------|----------|
|
|
|
+| 后端枚举 + DTO | Step 1 | 枚举新增、FlowProperties 扩展、DTO 新增 | 0.5h |
|
|
|
+| 手动升级 | Step 2 | Escalate 引擎方法 + API + 通知 | 2h |
|
|
|
+| 超时自动处理 | Step 3 | HandleTimeoutTask + Auto 系列方法 | 2h |
|
|
|
+| 定时任务 | Step 4 | FlowTimeoutJob | 1h |
|
|
|
+| 前端设计器 | Step 5 | PropertyCommon.vue 升级配置 UI | 2h |
|
|
|
+| 前端审批面板 | Step 6 | ApprovalPanel.vue 升级按钮 + 弹窗 | 1h |
|
|
|
+| 编译验证 | Step 7 | 前后端编译启动 | 0.5h |
|
|
|
+| 全量测试 | Step 8 | 新功能 + 全量回归 | 3h |
|
|
|
+| **合计** | | | **约 12h(1.5 天)** |
|