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);
}
}