|
|
@@ -70,9 +70,22 @@ public class FlowEngineService : ITransient
|
|
|
// ═══════════════════════════════════════════
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 发起流程
|
|
|
+ /// 发起流程(HTTP 人工调用)。租户上下文取自当前登录身份。
|
|
|
/// </summary>
|
|
|
- public async Task<long> StartFlow(StartFlowInput input)
|
|
|
+ public Task<long> StartFlow(StartFlowInput input) => StartFlowCore(input, _userManager.TenantId);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:发起流程(受信任租户重载)。
|
|
|
+ /// 仅限已在调用方完成租户归属校验的后台/系统上下文使用(如 S8 自动异常建单、
|
|
|
+ /// S8TimeoutAutoEscalationJob 触发的自动升级)——<paramref name="trustedTenantId"/>
|
|
|
+ /// 必须来自调用方已持有的可信业务租户(如 S8 规则/异常自身的 TenantId),
|
|
|
+ /// 禁止从客户端可提交的输入直接透传到本方法。
|
|
|
+ /// 无 HttpContext 的后台 Job 场景下 <c>_userManager.TenantId</c> 恒为 0,
|
|
|
+ /// 不使用本重载会导致流程定义选择与审批人解析全部失败(详见 S8 Flow Role 审计)。
|
|
|
+ /// </summary>
|
|
|
+ public Task<long> StartFlow(StartFlowInput input, long trustedTenantId) => StartFlowCore(input, trustedTenantId);
|
|
|
+
|
|
|
+ private async Task<long> StartFlowCore(StartFlowInput input, long effectiveTenantId)
|
|
|
{
|
|
|
// S8-S1-EXCEPTION-FLOW-SYNC-FIX-1:流程定义是「全局配置」,不应受 SqlSugarFilter 的数据范围(DataScope)隔离。
|
|
|
// ApprovalFlow 继承 EntityBaseOrgDel,会被数据范围过滤命中:
|
|
|
@@ -82,11 +95,15 @@ public class FlowEngineService : ITransient
|
|
|
// → 此查询返回 null → 抛「未找到已发布流程定义」→ StartFlow 失败 → 上游静默吞、建单不起流。
|
|
|
// ClearFilter() 清除该查询全部全局过滤(Org/Self 数据范围 + 软删);软删由 WHERE 显式 !IsDelete 补回;
|
|
|
// ApprovalFlow 无租户过滤(EntityBase 未实现 ITenantIdFilter),不存在绕过租户隔离风险。
|
|
|
- var tenantId = _userManager.TenantId;
|
|
|
+ //
|
|
|
+ // S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:TenantId 从「仅参与排序」改为「硬过滤」——
|
|
|
+ // 候选集合只允许「本租户流程」+「全局流程(TenantId IS NULL)」,其他租户的流程克隆
|
|
|
+ // (如 UATB-TB001/DEMO-TB001)绝不进入候选,避免跨租户流程定义串用。
|
|
|
var flow = await _flowRep.AsQueryable()
|
|
|
.ClearFilter()
|
|
|
- .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete)
|
|
|
- .OrderBy(u => u.TenantId == tenantId ? 1 : 0, OrderByType.Desc)
|
|
|
+ .Where(u => u.BizType == input.BizType && u.IsPublished && !u.IsDelete
|
|
|
+ && (u.TenantId == effectiveTenantId || u.TenantId == null))
|
|
|
+ .OrderBy(u => u.TenantId == effectiveTenantId ? 1 : 0, OrderByType.Desc)
|
|
|
.OrderBy(u => u.Version, OrderByType.Desc)
|
|
|
.FirstAsync() ?? throw Oops.Oh($"未找到业务类型 [{input.BizType}] 的已发布流程定义");
|
|
|
|
|
|
@@ -128,7 +145,7 @@ public class FlowEngineService : ITransient
|
|
|
throw Oops.Oh("流程图开始节点未连接任何后继节点");
|
|
|
foreach (var target in firstOutgoing)
|
|
|
{
|
|
|
- await ProcessNextNode(instance, flowData, target);
|
|
|
+ await ProcessNextNode(instance, flowData, target, effectiveTenantId);
|
|
|
}
|
|
|
|
|
|
await InvokeHandler(input.BizType, instance.Id, h => h.OnFlowStarted(input.BizId, instance.Id));
|
|
|
@@ -370,7 +387,8 @@ public class FlowEngineService : ITransient
|
|
|
ApproverIds = props.EscalationApproverIds,
|
|
|
ApproverNames = props.EscalationApproverNames,
|
|
|
},
|
|
|
- instance.InitiatorId);
|
|
|
+ instance.InitiatorId,
|
|
|
+ _userManager.TenantId);
|
|
|
|
|
|
if (escalationApprovers.Count == 0)
|
|
|
throw Oops.Oh("升级目标审批人列表为空");
|
|
|
@@ -575,13 +593,17 @@ public class FlowEngineService : ITransient
|
|
|
|
|
|
await CancelPendingTasks(task.InstanceId, task.NodeId, task.Id);
|
|
|
|
|
|
+ // S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:AutoEscalateTask 由 FlowTimeoutJob 后台调用,无 UserManager 上下文,
|
|
|
+ // _userManager.TenantId 在此恒为 0——这是既有的、超出本次 S8 异常建单批次范围的独立问题(登记见 KNOWN-ISSUES),
|
|
|
+ // 本次只将其显式化(原先是 ResolveApprovers 内部隐式读取,行为不变),不在本批修复。
|
|
|
var approvers = await ResolveApprovers(
|
|
|
new FlowProperties
|
|
|
{
|
|
|
ApproverType = nodeProps.EscalationApproverType,
|
|
|
ApproverIds = nodeProps.EscalationApproverIds,
|
|
|
},
|
|
|
- instance.InitiatorId);
|
|
|
+ instance.InitiatorId,
|
|
|
+ _userManager.TenantId);
|
|
|
|
|
|
if (approvers.Count == 0) return;
|
|
|
|
|
|
@@ -661,7 +683,12 @@ public class FlowEngineService : ITransient
|
|
|
/// - parallelGateway:Fork 并行分发;Join 等待所有前驱完成
|
|
|
/// - userTask / 其他:创建任务
|
|
|
/// </summary>
|
|
|
- private async Task ProcessNextNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nextNodeId)
|
|
|
+ /// <summary>
|
|
|
+ /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:<paramref name="trustedTenantId"/> 仅由 <see cref="StartFlowCore"/>
|
|
|
+ /// 的受信任路径向下透传;<see cref="AdvanceToNext"/>(Approve/超时自动通过等既有推进路径)调用时不传,
|
|
|
+ /// 保持原有语义——由 <see cref="CreateTasksForNode"/> 内部回退到 <c>_userManager.TenantId</c>。
|
|
|
+ /// </summary>
|
|
|
+ private async Task ProcessNextNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nextNodeId, long? trustedTenantId = null)
|
|
|
{
|
|
|
var nextNode = flowData.Nodes.FirstOrDefault(n => n.Id == nextNodeId);
|
|
|
if (nextNode == null)
|
|
|
@@ -688,7 +715,7 @@ public class FlowEngineService : ITransient
|
|
|
var targetNodeId = EvaluateGateway(nextNode.Properties?.Conditions, flowData, nextNode.Id, bizData);
|
|
|
instance.CurrentNodeId = targetNodeId;
|
|
|
await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
|
|
|
- await ProcessNextNode(instance, flowData, targetNodeId);
|
|
|
+ await ProcessNextNode(instance, flowData, targetNodeId, trustedTenantId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -712,7 +739,7 @@ public class FlowEngineService : ITransient
|
|
|
await MarkNodeCompleted(instance.Id, nextNode);
|
|
|
foreach (var target in outgoing)
|
|
|
{
|
|
|
- await ProcessNextNode(instance, flowData, target);
|
|
|
+ await ProcessNextNode(instance, flowData, target, trustedTenantId);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
@@ -720,7 +747,7 @@ public class FlowEngineService : ITransient
|
|
|
// userTask 或其他:创建任务
|
|
|
instance.CurrentNodeId = nextNodeId;
|
|
|
await _instanceRep.AsUpdateable(instance).UpdateColumns(i => new { i.CurrentNodeId }).ExecuteCommandAsync();
|
|
|
- await CreateTasksForNode(instance, flowData, nextNodeId);
|
|
|
+ await CreateTasksForNode(instance, flowData, nextNodeId, trustedTenantId);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -792,12 +819,15 @@ public class FlowEngineService : ITransient
|
|
|
return lastLog?.OperatorId > 0 ? lastLog.OperatorId : null;
|
|
|
}
|
|
|
|
|
|
- private async Task CreateTasksForNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nodeId)
|
|
|
+ private async Task CreateTasksForNode(ApprovalFlowInstance instance, ApprovalFlowItem flowData, string nodeId, long? trustedTenantId = null)
|
|
|
{
|
|
|
var node = flowData.Nodes.FirstOrDefault(n => n.Id == nodeId)
|
|
|
?? throw Oops.Oh($"FlowJson 中未找到节点 [{nodeId}]");
|
|
|
|
|
|
- var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId);
|
|
|
+ // S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:trustedTenantId 仅由 StartFlowCore 的受信任路径透传;
|
|
|
+ // 其余既有推进路径(Approve/ReturnToPrev/超时自动通过等)不传,回退到 _userManager.TenantId,行为不变。
|
|
|
+ var effectiveTenantId = trustedTenantId ?? _userManager.TenantId;
|
|
|
+ var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId, effectiveTenantId);
|
|
|
if (approvers.Count == 0)
|
|
|
throw Oops.Oh($"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空");
|
|
|
|
|
|
@@ -898,7 +928,12 @@ public class FlowEngineService : ITransient
|
|
|
await _taskRep.AsUpdateable(paired).ExecuteCommandAsync();
|
|
|
}
|
|
|
|
|
|
- private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId)
|
|
|
+ /// <summary>
|
|
|
+ /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:<paramref name="effectiveTenantId"/> 是本方法内所有租户相关判断
|
|
|
+ /// 的唯一真值源——Role Code → RoleId 查询、RoleId → 用户查询,一律使用它,不得再读 _userManager.TenantId。
|
|
|
+ /// 调用方负责传入正确值:HTTP 场景传 <c>_userManager.TenantId</c>;受信任后台场景传调用方已持有的业务租户。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId)
|
|
|
{
|
|
|
if (props == null || string.IsNullOrWhiteSpace(props.ApproverType))
|
|
|
return new List<(long, string)>();
|
|
|
@@ -934,7 +969,7 @@ public class FlowEngineService : ITransient
|
|
|
{
|
|
|
var codeRoleIds = await _userRep.Context.Queryable<SysRole>()
|
|
|
.ClearFilter()
|
|
|
- .Where(r => r.TenantId == _userManager.TenantId
|
|
|
+ .Where(r => r.TenantId == effectiveTenantId
|
|
|
&& r.Status == StatusEnum.Enable
|
|
|
&& codes.Contains(r.Code))
|
|
|
.Select(r => r.Id)
|
|
|
@@ -953,10 +988,10 @@ public class FlowEngineService : ITransient
|
|
|
// SysUser 继承 EntityBaseTenantOrg(→EntityBaseOrg),发起人 DataScope=Self 时被「CreateUserId==发起人」过滤、
|
|
|
// Dept/DeptChild 时被 OrgId 过滤,会把角色成员(甚至发起人自己,CreateUserId 可能为 NULL)过滤掉
|
|
|
// → 审批人列表为空 → ProcessNextNode 抛错、不建任务、实例悬挂。
|
|
|
- // ClearFilter() 清除数据范围过滤;显式补回租户隔离(TenantId==当前登录租户),等价于原全局租户过滤,无跨租户泄漏。
|
|
|
+ // ClearFilter() 清除数据范围过滤;显式补回租户隔离(TenantId==effectiveTenantId),等价于原全局租户过滤,无跨租户泄漏。
|
|
|
var users = await _userRep.AsQueryable()
|
|
|
.ClearFilter()
|
|
|
- .Where(u => userIds.Contains(u.Id) && u.TenantId == _userManager.TenantId)
|
|
|
+ .Where(u => userIds.Contains(u.Id) && u.TenantId == effectiveTenantId)
|
|
|
.ToListAsync();
|
|
|
return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
|
|
|
}
|