|
|
@@ -170,15 +170,22 @@ public class FlowTaskService : IDynamicApiController, ITransient
|
|
|
///
|
|
|
/// 这里刻意硬编码 BizType 字符串:ApprovalFlow 插件不引用 AiDOP 插件
|
|
|
/// (依赖方向为 AiDOP → ApprovalFlow,反向会成环),无法引用业务侧常量。
|
|
|
+ ///
|
|
|
+ /// <para><b>唯一 Authority Source</b>:某 BizType 的「是否只能走专用入口」以及
|
|
|
+ /// 「封堵范围延伸到哪些通用动作」都由本字典的同一条记录表达,不得再另立集合。
|
|
|
+ /// 判定一律走 <see cref="DedicatedEntryPolicy"/> 的显式字段,禁止从文案内容、
|
|
|
+ /// 字典顺序或 BizType 字面量前缀反推。</para>
|
|
|
/// </summary>
|
|
|
- private static readonly Dictionary<string, string> DedicatedEntryOnlyBizTypes = new()
|
|
|
+ private static readonly Dictionary<string, DedicatedEntryPolicy> DedicatedEntryOnlyBizTypes = new()
|
|
|
{
|
|
|
- ["S5_IQC_INSPBILL"] = "来料检验单请在「来料检验单详情」页完成审核,不支持在审批中心直接同意/拒绝。",
|
|
|
+ ["S5_IQC_INSPBILL"] = new("来料检验单请在「来料检验单详情」页完成审核,不支持在审批中心直接同意/拒绝。",
|
|
|
+ BlockFlowMutation: false),
|
|
|
// S6 过程检验单:N3_QE_DISPOSITION 的处置记录(ado_s6_ipqc_quality_disposition)是由
|
|
|
// S6ProcessInspectionReview.qe-disposition 在同一事务里 INSERT 后才推进流程的。走通用审批会把
|
|
|
// 流程直接推到 end 而永不写处置记录 —— 流程显示已闭环、处置轨迹却为空,且事后无法补记。
|
|
|
// 另:专用 supervisor-return 要求退回意见必填且回退到 N1,通用 Reject 会把实例直接置为终态。
|
|
|
- ["S6_PROCESS_INSPECTION"] = "过程检验单请在「过程检验」业务页完成主管审核或质量处置,不支持在审批中心直接同意/拒绝。",
|
|
|
+ ["S6_PROCESS_INSPECTION"] = new("过程检验单请在「过程检验」业务页完成主管审核或质量处置,不支持在审批中心直接同意/拒绝。",
|
|
|
+ BlockFlowMutation: false),
|
|
|
// S6 IPQC 过程检验单(源 qms_gcjyd,与上面 Phase1C 链同名不同源):同款旁路,且破坏面更大。
|
|
|
// ① N3_QUALITY_DISPOSITION 的处置记录(ado_s6_ipqc_quality_disposition)由
|
|
|
// IpqcInspectionDisposition.submit 在同一事务内 INSERT 后才推进流程;
|
|
|
@@ -186,7 +193,8 @@ public class FlowTaskService : IDynamicApiController, ITransient
|
|
|
// 走通用审批两者都不会发生 —— 流程显示已闭环,处置轨迹为空且业务状态永久停在旧值。
|
|
|
// 另:专用 supervisor-return 要求退回意见必填、经 ReturnToPrev 回到 N1,通用 Reject 直接置实例终态。
|
|
|
// 2026-09-14 实测:通用 approve 曾真实把一条 IPQC N2 待办推进到 end。
|
|
|
- ["IPQC_INSPECTION"] = "过程检验单请在「过程检验」业务页完成检验、主管审核或质量处置,不支持在审批中心直接同意/拒绝。",
|
|
|
+ ["IPQC_INSPECTION"] = new("过程检验单请在「过程检验」业务页完成检验、主管审核或质量处置,不支持在审批中心直接同意/拒绝。",
|
|
|
+ BlockFlowMutation: false),
|
|
|
// S7 成品检验单:本链的 IFlowBizHandler(FqcInspBillFlowBizHandler)三个回调**全是 no-op**,
|
|
|
// 业务副作用一律由专用 API 在引擎之外、同事务内完成,故通用审批绕过的是全部业务语义:
|
|
|
// ① N1 通用 approve 会在 submit-result 从未写入 pd/hgsl/bhgsl/clfs 的情况下推到 N2 ——
|
|
|
@@ -199,40 +207,35 @@ public class FlowTaskService : IDynamicApiController, ITransient
|
|
|
// 检验进度永远停在「检验完成」,且通用 Reject 连退回意见都不要求。
|
|
|
// ④ N3 通用 approve 推到 end 而 ado_s7_fqc_qe_disposition 永不写入 —— 流程显示闭环、
|
|
|
// 处置轨迹为空且事后无法补记(与 IPQC_INSPECTION 同款,该形态 2026-09-14 已实证发生过)。
|
|
|
- ["S7_FQC_INSPBILL"] = "成品检验单请在「FQC检验单详情」页完成检验结果提交、主管审核或质量处置,不支持在审批中心直接同意/拒绝/退回/撤回/转办/加签。",
|
|
|
+ //
|
|
|
+ // BlockFlowMutation=true 的唯一一条:除同意/拒绝外,退回上一步/撤回/转办/加签
|
|
|
+ // 四个变更型通用入口也一并封堵。理由同样源于「业务副作用不在引擎里」:
|
|
|
+ // ⑤ 通用 ReturnToPrev 绕过专用 supervisor-reject 的强制退回意见、同事务复位
|
|
|
+ // FINSPECTSTATUS、以及 mdp_std_fqc_task.inspect_progress 同步;从 N3 调用还会退到 N2,
|
|
|
+ // 产生专用设计从不产生的状态。
|
|
|
+ // ⑥ 通用 Withdraw 把实例置 Cancelled 而 OnFlowCompleted 为 no-op,报检单永久停在
|
|
|
+ // 「检验中」:重提结果被「流程已结束」拒、重新认领被「负责人已锁定」拒、
|
|
|
+ // 重新生成检验单被幂等唯一约束拒 —— 三条恢复路径全部关闭。
|
|
|
+ // ⑦ 通用 Transfer / AddSign 把 AssigneeId 原样写入(不校验存在性/租户/角色),
|
|
|
+ // 而专用 N2/N3 门禁是「持有本节点待办」而非「持有角色」,于是无
|
|
|
+ // ROLE_S7_FQC_SUPERVISOR / ROLE_S7_FQC_QE 的用户可直接完成主管审核或 QE 处置。
|
|
|
+ ["S7_FQC_INSPBILL"] = new("成品检验单请在「FQC检验单详情」页完成检验结果提交、主管审核或质量处置,不支持在审批中心直接同意/拒绝/退回/撤回/转办/加签。",
|
|
|
+ BlockFlowMutation: true),
|
|
|
};
|
|
|
|
|
|
/// <summary>
|
|
|
- /// S7-FQC-GENERIC-FLOW-MUTATION-GUARD-1:<see cref="DedicatedEntryOnlyBizTypes"/> 中**同时**需要
|
|
|
- /// 封堵变更型通用入口(退回上一步 / 撤回 / 转办 / 加签)的 BizType 子集。
|
|
|
- ///
|
|
|
- /// <para>这不是第二套名单——名单与文案的唯一来源仍是 <see cref="DedicatedEntryOnlyBizTypes"/>,
|
|
|
- /// 本集合只表达「该 BizType 的封堵范围是否延伸到变更型入口」这一作用域,
|
|
|
- /// 故必须是上面字典键的子集(由契约测试断言)。</para>
|
|
|
- ///
|
|
|
- /// <para><b>为什么只有 S7</b>:其余三条目前仅封堵同意/拒绝,把它们一并纳入会改变
|
|
|
- /// S5/S6/IPQC 的现有行为,超出本批范围。它们是否也该纳入需各自独立评估,
|
|
|
- /// 不在此顺手扩大。</para>
|
|
|
- ///
|
|
|
- /// <para><b>为什么 S7 必须纳入</b>:S7 的 <c>IFlowBizHandler</c> 三个回调全是 no-op,
|
|
|
- /// 业务副作用一律由 <c>FqcInspBillFlowService</c> 在引擎之外、同事务内完成,因此
|
|
|
- /// 任何通用流程变更都只改流程、不改业务:
|
|
|
- /// <list type="bullet">
|
|
|
- /// <item><b>退回上一步</b>绕过专用 <c>supervisor-reject</c> 的三件事——强制退回意见、
|
|
|
- /// 同事务复位 <c>qms_fqcbj.FINSPECTSTATUS='检验中'</c>、同步 <c>mdp_std_fqc_task.inspect_progress</c>;
|
|
|
- /// 且从 N3 调用会退到 N2,产生专用设计从不产生的状态。</item>
|
|
|
- /// <item><b>撤回</b>把实例置 Cancelled,而 <c>OnFlowCompleted</c> 为 no-op → 报检单永久停在
|
|
|
- /// 「检验中」:重提结果被「流程已结束」拒、重新认领被「负责人已锁定」拒、重新生成检验单被
|
|
|
- /// 幂等唯一约束拒,三条恢复路径全部关闭。</item>
|
|
|
- /// <item><b>转办 / 加签</b>把 <c>AssigneeId</c> 原样写入(不校验存在性/租户/角色),而专用
|
|
|
- /// N2/N3 的门禁是「持有本节点待办」而非「持有角色」,于是无 <c>ROLE_S7_FQC_SUPERVISOR</c> /
|
|
|
- /// <c>ROLE_S7_FQC_QE</c> 的用户可直接完成主管审核或 QE 处置。</item>
|
|
|
- /// </list></para>
|
|
|
+ /// 单条 BizType 的专用入口策略。<b>与 BizType 同属一条记录</b>——封堵范围不再由
|
|
|
+ /// 另一个集合表达,避免出现第二份 BizType authority source。
|
|
|
/// </summary>
|
|
|
- private static readonly HashSet<string> FlowMutationDedicatedOnlyBizTypes = new(StringComparer.Ordinal)
|
|
|
- {
|
|
|
- "S7_FQC_INSPBILL",
|
|
|
- };
|
|
|
+ /// <param name="Message">命中时对外抛出的引导文案。同一条 BizType 的所有通用入口共用它。</param>
|
|
|
+ /// <param name="BlockFlowMutation">
|
|
|
+ /// 是否把封堵范围从「同意 / 拒绝」延伸到变更型通用入口
|
|
|
+ /// (退回上一步 / 撤回 / 转办 / 加签)。
|
|
|
+ /// <para><c>false</c> 表示该 BizType 目前只封堵同意/拒绝,变更型入口保持既有行为——
|
|
|
+ /// S5 / S6 / IPQC 三条即如此:它们的绕过理由与 S7 同源,很可能也应纳入,
|
|
|
+ /// 但纳入会改变其现有行为,需各自独立评估,不在此顺手扩大。</para>
|
|
|
+ /// </param>
|
|
|
+ private sealed record DedicatedEntryPolicy(string Message, bool BlockFlowMutation);
|
|
|
|
|
|
/// <summary>
|
|
|
/// 通用审批动作前置守卫:按 taskId 从库中反查权威 BizType,命中名单即拒绝。
|
|
|
@@ -249,12 +252,12 @@ public class FlowTaskService : IDynamicApiController, ITransient
|
|
|
var instance = await _instanceRep.GetByIdAsync(task.InstanceId);
|
|
|
if (instance?.BizType == null) return;
|
|
|
if (DedicatedEntryOnlyBizTypes.TryGetValue(instance.BizType, out var hint))
|
|
|
- throw Oops.Oh(hint);
|
|
|
+ throw Oops.Oh(hint.Message);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 变更型通用入口(退回上一步 / 转办 / 加签)的前置守卫:按 taskId 反查权威 BizType,
|
|
|
- /// 命中 <see cref="FlowMutationDedicatedOnlyBizTypes"/> 即拒绝。
|
|
|
+ /// 命中且 <see cref="DedicatedEntryPolicy.BlockFlowMutation"/> 为真即拒绝。
|
|
|
///
|
|
|
/// <para>必须作为动作方法的第一条语句调用——引擎侧这些动作没有 <c>[UnitOfWork]</c>,
|
|
|
/// 一旦进入就会按「取消同节点待办 → 改 task 状态 → 改 CurrentNodeId → 建新任务」的顺序
|
|
|
@@ -270,14 +273,18 @@ public class FlowTaskService : IDynamicApiController, ITransient
|
|
|
/// <summary>
|
|
|
/// 同上,但按 instanceId 判定——撤回(Withdraw)的入参只有 instanceId,没有 taskId。
|
|
|
/// 只做只读反查,不改任何实体。
|
|
|
+ ///
|
|
|
+ /// <para>判定读的是同一份 <see cref="DedicatedEntryOnlyBizTypes"/> 里该 BizType 记录的
|
|
|
+ /// <see cref="DedicatedEntryPolicy.BlockFlowMutation"/> 显式字段,
|
|
|
+ /// 不查任何第二个集合、不从文案内容或 BizType 字面量反推。</para>
|
|
|
/// </summary>
|
|
|
private async Task EnsureDedicatedFlowMutationOnlyByInstanceAsync(long instanceId)
|
|
|
{
|
|
|
var instance = await _instanceRep.GetByIdAsync(instanceId);
|
|
|
if (instance?.BizType == null) return;
|
|
|
- if (!FlowMutationDedicatedOnlyBizTypes.Contains(instance.BizType)) return;
|
|
|
- if (DedicatedEntryOnlyBizTypes.TryGetValue(instance.BizType, out var mutationHint))
|
|
|
- throw Oops.Oh(mutationHint);
|
|
|
+ if (!DedicatedEntryOnlyBizTypes.TryGetValue(instance.BizType, out var policy)) return;
|
|
|
+ if (!policy.BlockFlowMutation) return;
|
|
|
+ throw Oops.Oh(policy.Message);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|