using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse; namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8; /// /// S8-RULE-READINESS-1:「这个部门 Id 在当前租户下是否可用」的唯一判定实现。 /// /// 为什么必须只有一份:同一个判据现在有两个调用方 —— /// 启用前的就绪门禁(S8RuleReadinessGate)与运行时的自动建单 /// (S8ManualReportService.ResolveAutoWatchDepartmentsAsync)。 /// 两者一旦各写一份,就会出现最坏的那种结局:启用检查通过、建单却失败, /// 而管理员在页面上只看到「已启用 / 最近结果:成功 / 异常 0 条」,没有任何线索。 /// 这正是本轮要消灭的静默失败,所以判据必须物理同源,不是"两处写成一样"。 /// /// 判据三条,缺一不可:主键命中 · 属于该租户 · IsActive。 /// 工厂不参与 —— 真库 factory_ref_id = 1000 横跨两个租户 /// (824585161322565 / 797403760988229),按工厂校验会把别家的部门判为合法。 /// /// ClearFilter():边界由本方法显式声明,不依赖全局 multi-tenant AOP 的当前行为, /// 结果与是否开启该过滤器无关(两者判据同为 tenant_id == 传入租户)。 /// public interface IS8DepartmentScopeValidator { /// 部门是否存在于该租户且启用。 Task ExistsInTenantAsync(long? deptId, long tenantId); } /// /// 正式实现。生产代码里只有这一份 —— 接口的存在是为了给测试留 seam, /// 不是为了允许出现第二种判据。 /// public sealed class S8DepartmentScopeValidator : IS8DepartmentScopeValidator, ITransient { private readonly SqlSugarRepository _deptRep; public S8DepartmentScopeValidator(SqlSugarRepository deptRep) => _deptRep = deptRep; /// /// 部门是否存在于该租户且启用。deptId 为空 / 非正数、或 tenantId 非正数一律返回 false /// —— 调用方需要区分「没配」与「配了但非法」时,请在调用前自行判空,本方法不承担该语义。 /// public async Task ExistsInTenantAsync(long? deptId, long tenantId) { if (!deptId.HasValue || deptId.Value <= 0 || tenantId <= 0) return false; return await _deptRep.AsQueryable().ClearFilter() .Where(x => x.Id == deptId.Value && x.TenantId == tenantId && x.IsActive) .AnyAsync(); } }