using Admin.NET.Plugin.ApprovalFlow; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse; /// /// S5 IQC 检验单「项目级结果是否可编辑」的唯一判据(Batch 3)。 /// /// 单独成类而非散落在 Service 内,目的是让编辑授权只有一个真源: /// Detail 的 canEdit、save-detail 的门禁都必须走这里,不允许各处自行判 currentNode。 /// /// 【判据】 /// ① bill 必须存在且属当前租户(带租户谓词,见 tenant 边界说明) /// ② 取该 bill 最新流程实例(BizType + BizId) /// · 实例不存在(尚未起流) → 具备检验员角色即可编辑 /// · 实例非 Running(终态/拒绝/撤销)→ 只读 /// · Running 且当前用户有 N1_INSPECT 待办 → 可编辑 /// · 其余(N2 / GW_PD / N3 / end;或在 N1 但待办属他人)→ 只读 /// /// 【为什么必须有「实例不存在」分支】 /// GenerateInspBill **不起流**,流程是 submit-result 惰性 StartFlow /// (IqcInspBillFlowService.cs:95-107)。新生成的检验单没有实例、也没有 N1 待办, /// 若只认「有实例且在 N1」,检验员将永远无法在提交前录入结果,本能力直接失效。 /// 该分支与既有 IqcInspBillFlowService.BuildStateAsync 对 CanSubmitResult 的判法 /// (实例为空时 = HasRole(RoleInspectorId))完全一致 —— 保证「能存」与「能提交」 /// 权限口径同一,不会出现可存不可提交、或可提交不可存的矛盾。 /// /// 【为什么不叠加 jyfzr == 当前用户】 /// jyfzr 在报检分录上、由认领/调配写入;ApprovalFlow 的 N1 受让人由角色成员解析, /// 两者是不同人群。同时要求会制造与 S7 FQC 同类的审批人死结。 /// N1 待办本身已是更强的授权凭据,故刻意不加该条件。 /// /// 【租户边界的结构性限制(实测)】 /// ApprovalFlowInstance / ApprovalFlowTask **没有 TenantId 列**(只有 OrgId / CreateUserId), /// 因此流程查询无法直接加租户谓词。本类的做法是:先用带 tenant_id 谓词的 bill 查询确认 /// billId 属当前租户,再以该 billId 作 BizId 查流程 —— 租户约束经 bill 传递。 /// 这是该框架表结构下可达的最强隔离。 /// public class IqcInspBillEditGate : ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public IqcInspBillEditGate(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } /// /// 该检验单当前是否允许由**当前用户**编辑项目级结果。 /// billId 不属当前租户时返回 false(不区分“不存在”与“属他人租户”,避免存在性泄漏)。 /// public async Task CanEditAsync(long billId, long tenantId) { if (billId <= 0 || tenantId <= 0) return false; // ① 租户归属(唯一的租户谓词落点;后续流程查询经 billId 传递该约束) var owns = await _db.Ado.GetIntAsync( "SELECT COUNT(1) FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId", new SugarParameter("@id", billId), new SugarParameter("@TenantId", tenantId)); if (owns == 0) return false; // ② 最新流程实例 var inst = await _db.Queryable() .ClearFilter() .Where(x => x.BizType == IqcInspBillFlowConst.BizType && x.BizId == billId) .OrderByDescending(x => x.Id) .FirstAsync(); // 尚未起流:检验员可先录入,提交时再惰性起流 if (inst == null) return await HasInspectorRoleAsync(); // 已结束(Approved / Rejected / Cancelled / Terminated)→ 只读 if (inst.Status != FlowInstanceStatusEnum.Running) return false; // Running:必须当前用户在 N1_INSPECT 有待办 var userId = _userManager.UserId; var myTask = await _db.Queryable() .ClearFilter() .Where(x => x.InstanceId == inst.Id && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending) .FirstAsync(); return myTask != null && myTask.NodeId == IqcInspBillFlowConst.NodeInspect; } /// 当前用户是否具备 S5 IQC 检验员角色(跨数据范围,与 Flow 服务同口径)。 private async Task HasInspectorRoleAsync() { var userId = _userManager.UserId; return await _db.Queryable() .ClearFilter() .AnyAsync(x => x.UserId == userId && x.RoleId == IqcInspBillFlowConst.RoleInspectorId); } }