浏览代码

fix: S0 仓储剩余7个A类Controller多租户P0隔离 (server 1.0.295)

移除 [AllowAnonymous],复用 AdoS0TenantScope.TryResolveRequired 显式租户门禁,
不依赖全局 AOP、无默认租户回退、不读前端 payload tenantId。

涉及 cost-centers/label-types/item-packs/departments/employees/nbr-controls/
emp-work-duties 七个控制器:List/GetById/Create/Update/Delete 全部按当前租户
限定;唯一校验含 tenant;员工→部门、物料职责→员工/库位关联父校验限当前租户;
部门/员工/库位补显限当前租户;删除引用检查限当前租户。

AdoS0ReferenceChecker 新增 Department/Employee ExistsInTenantAsync,
Department/Employee ReferencesAsync 增 tenantId 参数。

已知 P1(本批不改):ItemPackMaster/NbrControl 唯一索引不含 tenant_id。
YY968XX 6 天之前
父节点
当前提交
ced03d90d4

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.294</AssemblyVersion>
-    <FileVersion>1.0.294</FileVersion>
-    <Version>1.0.294</Version>
+    <AssemblyVersion>1.0.295</AssemblyVersion>
+    <FileVersion>1.0.295</FileVersion>
+    <Version>1.0.295</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 17 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0CostCentersController.cs

@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 成本中心主数据(CostCtrMaster 语义)
+/// S0 成本中心主数据(CostCtrMaster 语义)。
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/cost-centers")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0CostCentersController : ControllerBase
 {
@@ -23,9 +23,11 @@ public class AdoS0CostCentersController : ControllerBase
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0CostCtrQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -45,18 +47,22 @@ public class AdoS0CostCentersController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         return item == null ? NotFound() : Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0CostCtrUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "成本中心编码已存在");
 
         var entity = new AdoS0CostCtrMaster
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -75,12 +81,15 @@ public class AdoS0CostCentersController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0CostCtrUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.CostCtr == dto.CostCtr))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "成本中心编码已存在");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -98,7 +107,8 @@ public class AdoS0CostCentersController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });

+ 20 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0DepartmentsController.cs

@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 部门主数据(DepartmentMaster 语义)
+/// S0 部门主数据(DepartmentMaster 语义)。
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/departments")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0DepartmentsController : ControllerBase
 {
@@ -25,9 +25,11 @@ public class AdoS0DepartmentsController : ControllerBase
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0DepartmentQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -48,18 +50,22 @@ public class AdoS0DepartmentsController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         return item == null ? NotFound() : Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0DepartmentUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "部门编码已存在");
 
         var entity = new AdoS0DepartmentMaster
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -77,12 +83,15 @@ public class AdoS0DepartmentsController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0DepartmentUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Department == dto.Department))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "部门编码已存在");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -99,10 +108,13 @@ public class AdoS0DepartmentsController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
 
-        var refInfo = await _refChecker.DepartmentReferencesAsync(item.Department);
+        // 引用检查限定当前租户:不因他租户同名部门被引用而阻止/误判
+        var refInfo = await _refChecker.DepartmentReferencesAsync(tenantId, item.Department);
         if (refInfo is { } r)
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DeleteBlocked,
                 $"存在 {r.Count} 条 {r.Table} 引用该部门,无法删除");

+ 47 - 11
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmpWorkDutiesController.cs

@@ -5,34 +5,39 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 物料职责维护(EmpWorkDutyMaster 语义)
+/// S0 物料职责维护(EmpWorkDutyMaster 语义)。员工 + 库位关联。
+/// 多租户隔离:所有读写显式限定当前请求租户;员工/库位存在性与补显均限定当前租户,杜绝跨租户绑定;不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/emp-work-duties")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0EmpWorkDutiesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0EmpWorkDutyMaster> _rep;
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
     private readonly SqlSugarRepository<AdoS0LocationMaster> _locRep;
+    private readonly AdoS0ReferenceChecker _refChecker;
 
     public AdoS0EmpWorkDutiesController(
         SqlSugarRepository<AdoS0EmpWorkDutyMaster> rep,
         SqlSugarRepository<AdoS0EmployeeMaster> empRep,
-        SqlSugarRepository<AdoS0LocationMaster> locRep)
+        SqlSugarRepository<AdoS0LocationMaster> locRep,
+        AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
         _empRep = empRep;
         _locRep = locRep;
+        _refChecker = refChecker;
     }
 
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0EmpWorkDutyQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -55,7 +60,7 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
             .Take(q.PageSize)
             .ToListAsync();
 
-        await ApplyDisplayFieldsAsync(list);
+        await ApplyDisplayFieldsAsync(list, tenantId);
 
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
@@ -63,20 +68,27 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
-        await ApplyDisplayFieldsAsync(new List<AdoS0EmpWorkDutyMaster> { item });
+        await ApplyDisplayFieldsAsync(new List<AdoS0EmpWorkDutyMaster> { item }, tenantId);
         return Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0EmpWorkDutyUpsertDto dto)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
         var scopeError = ValidateMaterialScope(dto, out var itemNum, out var itemNum1, out var itemNum2);
         if (scopeError != null) return AdoS0ApiErrors.InvalidRequest(scopeError);
 
+        var relError = await ValidateRelationsInTenantAsync(tenantId, dto);
+        if (relError != null) return relError;
+
         var entity = new AdoS0EmpWorkDutyMaster
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -100,12 +112,18 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0EmpWorkDutyUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
         var scopeError = ValidateMaterialScope(dto, out var itemNum, out var itemNum1, out var itemNum2);
         if (scopeError != null) return AdoS0ApiErrors.InvalidRequest(scopeError);
 
+        var relError = await ValidateRelationsInTenantAsync(tenantId, dto);
+        if (relError != null) return relError;
+
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -128,12 +146,29 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });
     }
 
+    /// <summary>员工与库位必须属于当前租户(同 domain),杜绝为他租户员工分配职责/绑定他租户库位。</summary>
+    private async Task<IActionResult?> ValidateRelationsInTenantAsync(long tenantId, AdoS0EmpWorkDutyUpsertDto dto)
+    {
+        if (!string.IsNullOrWhiteSpace(dto.Employee)
+            && !await _refChecker.EmployeeExistsInTenantAsync(tenantId, dto.DomainCode, dto.Employee))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"员工编码 '{dto.Employee}' 不存在于当前租户雇员主数据");
+
+        if (!string.IsNullOrWhiteSpace(dto.Location)
+            && !await _refChecker.LocationExistsInTenantAsync(tenantId, dto.DomainCode, dto.Location))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"库位编码 '{dto.Location}' 不存在于当前租户库位主数据");
+
+        return null;
+    }
+
     private static readonly Dictionary<string, string> DutyMap = new(StringComparer.OrdinalIgnoreCase)
     {
         ["iss-po"] = "采购收货",
@@ -145,18 +180,19 @@ public class AdoS0EmpWorkDutiesController : ControllerBase
         ["Up-Shelf"] = "上架"
     };
 
-    private async Task ApplyDisplayFieldsAsync(List<AdoS0EmpWorkDutyMaster> duties)
+    /// <summary>补显员工名 / 库位名。关联 EmployeeMaster、LocationMaster 均限定当前租户,避免跨租户补错。</summary>
+    private async Task ApplyDisplayFieldsAsync(List<AdoS0EmpWorkDutyMaster> duties, long tenantId)
     {
         if (duties.Count == 0) return;
 
         var domainCodes = duties.Select(d => d.DomainCode).Distinct().ToList();
 
         var employees = await _empRep.AsQueryable()
-            .Where(e => domainCodes.Contains(e.DomainCode))
+            .Where(e => e.TenantId == tenantId && domainCodes.Contains(e.DomainCode))
             .ToListAsync();
 
         var locations = await _locRep.AsQueryable()
-            .Where(l => domainCodes.Contains(l.DomainCode))
+            .Where(l => l.TenantId == tenantId && domainCodes.Contains(l.DomainCode))
             .ToListAsync();
 
         foreach (var duty in duties)

+ 33 - 60
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs

@@ -1,42 +1,40 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
-using Microsoft.Extensions.Logging;
 
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 雇员主数据(EmployeeMaster 语义)
+/// S0 雇员主数据(EmployeeMaster 语义)。
+/// 多租户隔离:所有读写显式限定当前请求租户;部门父校验与部门名补显均限定当前租户;不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/employees")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0EmployeesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _rep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
     private readonly AdoS0ReferenceChecker _refChecker;
-    private readonly ILogger<AdoS0EmployeesController> _logger;
 
     public AdoS0EmployeesController(
         SqlSugarRepository<AdoS0EmployeeMaster> rep,
         SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
-        AdoS0ReferenceChecker refChecker,
-        ILogger<AdoS0EmployeesController> logger)
+        AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
         _deptRep = deptRep;
         _refChecker = refChecker;
-        _logger = logger;
     }
 
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0EmployeeQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -52,7 +50,7 @@ public class AdoS0EmployeesController : ControllerBase
             .Take(q.PageSize)
             .ToListAsync();
 
-        await ApplyDeptDescrAsync(list);
+        await ApplyDeptDescrAsync(list, tenantId);
 
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
@@ -60,31 +58,29 @@ public class AdoS0EmployeesController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
-        await ApplyDeptDescrAsync(new List<AdoS0EmployeeMaster> { item });
+        await ApplyDeptDescrAsync(new List<AdoS0EmployeeMaster> { item }, tenantId);
         return Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0EmployeeUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
-        // B2:Create 一律严格作用域校验,禁止降级(参 AdoS0MfgRoutingOpDetailsController)。
-        var deptResult = await _refChecker.DepartmentExistsInScopeAsync(dto.Department, dto.CompanyRefId, dto.FactoryRefId);
-        switch (deptResult)
-        {
-            case MaterialScopeCheck.NotFound:
-                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                    $"部门编码 '{dto.Department}' 不存在于部门主数据");
-            case MaterialScopeCheck.ScopeMiss:
-                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
-                    $"部门编码 '{dto.Department}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
-        }
+        // 所属部门必须存在于当前租户(同 domain + department),杜绝员工绑定他租户部门
+        if (!string.IsNullOrWhiteSpace(dto.Department)
+            && !await _refChecker.DepartmentExistsInTenantAsync(tenantId, dto.DomainCode, dto.Department))
+            return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
+                $"部门编码 '{dto.Department}' 不存在于当前租户部门主数据");
 
         var entity = MapToEntity(dto);
+        entity.TenantId = tenantId;
         entity.CreateTime = DateTime.Now;
         await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
         return Ok(entity);
@@ -93,46 +89,20 @@ public class AdoS0EmployeesController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0EmployeeUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Employee == dto.Employee))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "员工编码已存在");
 
-        // B2 + 历史兼容降级(D-03 严格条件,参 AdoS0MfgRoutingOpDetailsController)
-        var deptResult = await _refChecker.DepartmentExistsInScopeAsync(dto.Department, dto.CompanyRefId, dto.FactoryRefId);
-        if (deptResult == MaterialScopeCheck.NotFound)
+        if (!string.IsNullOrWhiteSpace(dto.Department)
+            && !await _refChecker.DepartmentExistsInTenantAsync(tenantId, dto.DomainCode, dto.Department))
             return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.ReferenceNotFound,
-                $"部门编码 '{dto.Department}' 不存在于部门主数据");
-
-        if (deptResult == MaterialScopeCheck.ScopeMiss)
-        {
-            // 降级条件(全部满足才允许):
-            //   1. 是 Update(已满足)
-            //   2. DB 原记录 scope 为 0/0
-            //   3. 本次未修改 Department 引用值
-            var originScopeEmpty = entity.CompanyRefId == 0 && entity.FactoryRefId == 0;
-            var departmentUnchanged = string.Equals(entity.Department, dto.Department?.Trim(), StringComparison.Ordinal);
-
-            if (originScopeEmpty && departmentUnchanged)
-            {
-                _logger.LogWarning(
-                    "[B2 Downgrade] Table={TableName} PrimaryKey={PrimaryKey} ReferenceField={ReferenceField} " +
-                    "OldValue={OldValue} NewValue={NewValue} CompanyRefId={CompanyRefId} FactoryRefId={FactoryRefId} " +
-                    "DowngradeReason={DowngradeReason}",
-                    "EmployeeMaster", entity.Id, "Department",
-                    entity.Department, dto.Department,
-                    dto.CompanyRefId, dto.FactoryRefId,
-                    "LegacyRecord_OriginScopeZero_DepartmentUnchanged");
-                // 放行,继续执行写入
-            }
-            else
-            {
-                return AdoS0ApiErrors.InvalidReference(AdoS0ErrorCodes.InvalidReferenceScope,
-                    $"部门编码 '{dto.Department}' 不属于当前公司/工厂 (CompanyRefId={dto.CompanyRefId}, FactoryRefId={dto.FactoryRefId})");
-            }
-        }
+                $"部门编码 '{dto.Department}' 不存在于当前租户部门主数据");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -162,10 +132,12 @@ public class AdoS0EmployeesController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
 
-        var refInfo = await _refChecker.EmployeeReferencesAsync(item.Employee);
+        var refInfo = await _refChecker.EmployeeReferencesAsync(tenantId, item.Employee);
         if (refInfo is { } r)
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DeleteBlocked,
                 $"存在 {r.Count} 条 {r.Table} 引用该雇员,无法删除");
@@ -198,13 +170,14 @@ public class AdoS0EmployeesController : ControllerBase
         CreateUser = dto.CreateUser
     };
 
-    private async Task ApplyDeptDescrAsync(List<AdoS0EmployeeMaster> employees)
+    /// <summary>补显部门说明。关联 DepartmentMaster 限定当前租户,避免同编码部门跨租户补错描述。</summary>
+    private async Task ApplyDeptDescrAsync(List<AdoS0EmployeeMaster> employees, long tenantId)
     {
         if (employees.Count == 0) return;
 
         var domainCodes = employees.Select(e => e.DomainCode).Distinct().ToList();
         var depts = await _deptRep.AsQueryable()
-            .Where(d => domainCodes.Contains(d.DomainCode))
+            .Where(d => d.TenantId == tenantId && domainCodes.Contains(d.DomainCode))
             .ToListAsync();
 
         foreach (var emp in employees)

+ 17 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0ItemPacksController.cs

@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 零件包装规格(ItemPackMaster 语义)
+/// S0 零件包装规格(ItemPackMaster 语义)。
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/item-packs")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0ItemPacksController : ControllerBase
 {
@@ -23,9 +23,11 @@ public class AdoS0ItemPacksController : ControllerBase
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ItemPackQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -47,18 +49,22 @@ public class AdoS0ItemPacksController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         return item == null ? NotFound() : Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0ItemPackUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.ItemNum == dto.ItemNum))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.ItemNum == dto.ItemNum))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "物料包装规格已存在");
 
         var entity = new AdoS0ItemPackMaster
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -86,12 +92,15 @@ public class AdoS0ItemPacksController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ItemPackUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.ItemNum == dto.ItemNum))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.ItemNum == dto.ItemNum))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "物料包装规格已存在");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -118,7 +127,8 @@ public class AdoS0ItemPacksController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });

+ 17 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LabelTypesController.cs

@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 标签格式头表(BarCodeType 语义)
+/// S0 标签格式头表(BarCodeType 语义)。
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/label-types")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0LabelTypesController : ControllerBase
 {
@@ -23,9 +23,11 @@ public class AdoS0LabelTypesController : ControllerBase
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0LabelTypeQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -46,18 +48,22 @@ public class AdoS0LabelTypesController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         return item == null ? NotFound() : Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0LabelTypeUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "标签类型编码已存在");
 
         var entity = new AdoS0BarCodeType
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -75,12 +81,15 @@ public class AdoS0LabelTypesController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0LabelTypeUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.BarType == dto.BarType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "标签类型编码已存在");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -97,7 +106,8 @@ public class AdoS0LabelTypesController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });

+ 17 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0NbrControlsController.cs

@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
 namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
 
 /// <summary>
-/// S0 单号规则主数据(NbrControl 语义)
+/// S0 单号规则主数据(NbrControl 语义)。仅本维护页 CRUD;实际取号消费方另在其它模块,不在本 Controller。
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
 /// </summary>
 [ApiController]
 [Route("api/s0/warehouse/nbr-controls")]
-[AllowAnonymous]
 [NonUnify]
 public class AdoS0NbrControlsController : ControllerBase
 {
@@ -23,9 +23,11 @@ public class AdoS0NbrControlsController : ControllerBase
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0NbrControlQueryDto q)
     {
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
 
         var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == tenantId)
             .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
             .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
             .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
@@ -45,18 +47,22 @@ public class AdoS0NbrControlsController : ControllerBase
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         return item == null ? NotFound() : Ok(item);
     }
 
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0NbrControlUpsertDto dto)
     {
-        if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.NbrType == dto.NbrType))
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.NbrType == dto.NbrType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "单号类型编码已存在");
 
         var entity = new AdoS0NbrControl
         {
+            TenantId = tenantId,
             CompanyRefId = dto.CompanyRefId,
             FactoryRefId = dto.FactoryRefId,
             DomainCode = dto.DomainCode ?? string.Empty,
@@ -84,12 +90,15 @@ public class AdoS0NbrControlsController : ControllerBase
     [HttpPut("{id:long}")]
     public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0NbrControlUpsertDto dto)
     {
-        var entity = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+
+        var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (entity == null) return NotFound();
 
-        if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.NbrType == dto.NbrType))
+        if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.NbrType == dto.NbrType))
             return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "单号类型编码已存在");
 
+        // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
         entity.CompanyRefId = dto.CompanyRefId;
         entity.FactoryRefId = dto.FactoryRefId;
         entity.DomainCode = dto.DomainCode ?? string.Empty;
@@ -116,7 +125,8 @@ public class AdoS0NbrControlsController : ControllerBase
     [HttpDelete("{id:long}")]
     public async Task<IActionResult> DeleteAsync(long id)
     {
-        var item = await _rep.GetByIdAsync(id);
+        if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
+        var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });

+ 29 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AdoS0ReferenceChecker.cs

@@ -268,6 +268,30 @@ public sealed class AdoS0ReferenceChecker
             .AnyAsync();
     }
 
+    /// <summary>
+    /// 租户内部门存在性检查:当前租户 + domain + department 命中即视为父部门存在。
+    /// 供员工写入的部门校验(显式限定租户,杜绝员工绑定他租户部门)。
+    /// </summary>
+    public async Task<bool> DepartmentExistsInTenantAsync(long tenantId, string? domainCode, string? department)
+    {
+        if (string.IsNullOrWhiteSpace(department)) return false;
+        return await _db.Queryable<AdoS0DepartmentMaster>()
+            .Where(x => x.TenantId == tenantId && x.DomainCode == domainCode && x.Department == department)
+            .AnyAsync();
+    }
+
+    /// <summary>
+    /// 租户内员工存在性检查:当前租户 + domain + employee 命中即视为员工存在。
+    /// 供物料职责写入的员工校验(显式限定租户,杜绝为他租户员工分配职责)。
+    /// </summary>
+    public async Task<bool> EmployeeExistsInTenantAsync(long tenantId, string? domainCode, string? employee)
+    {
+        if (string.IsNullOrWhiteSpace(employee)) return false;
+        return await _db.Queryable<AdoS0EmployeeMaster>()
+            .Where(x => x.TenantId == tenantId && x.DomainCode == domainCode && x.Employee == employee)
+            .AnyAsync();
+    }
+
     /// <summary>
     /// 库位(LocationMaster)删除前引用检查(租户隔离版)。
     /// 覆盖下游:货架(显式限定 tenant+domain+location,不因他租户同码货架而阻止/误判)/ 物料默认库位 / 产线 6 个 Location 变体。
@@ -306,17 +330,17 @@ public sealed class AdoS0ReferenceChecker
     /// 部门(DepartmentMaster)删除前引用检查。
     /// 覆盖下游:员工所属部门 / 工作中心所属部门。
     /// </summary>
-    public async Task<(int Count, string Table)?> DepartmentReferencesAsync(string? department)
+    public async Task<(int Count, string Table)?> DepartmentReferencesAsync(long tenantId, string? department)
     {
         if (string.IsNullOrWhiteSpace(department)) return null;
 
         var emp = await _db.Queryable<AdoS0EmployeeMaster>()
-            .Where(x => x.Department == department)
+            .Where(x => x.TenantId == tenantId && x.Department == department)
             .CountAsync();
         if (emp > 0) return (emp, "员工 (EmployeeMaster)");
 
         var wc = await _db.Queryable<AdoS0WorkCtrMaster>()
-            .Where(x => x.Department == department)
+            .Where(x => x.TenantId == tenantId && x.Department == department)
             .CountAsync();
         if (wc > 0) return (wc, "工作中心 (WorkCtrMaster)");
 
@@ -365,12 +389,12 @@ public sealed class AdoS0ReferenceChecker
     /// 雇员(EmployeeMaster)删除前引用检查。
     /// 覆盖下游:物料职责(EmpWorkDutyMaster.Employee)。
     /// </summary>
-    public async Task<(int Count, string Table)?> EmployeeReferencesAsync(string? employee)
+    public async Task<(int Count, string Table)?> EmployeeReferencesAsync(long tenantId, string? employee)
     {
         if (string.IsNullOrWhiteSpace(employee)) return null;
 
         var duty = await _db.Queryable<AdoS0EmpWorkDutyMaster>()
-            .Where(x => x.Employee == employee)
+            .Where(x => x.TenantId == tenantId && x.Employee == employee)
             .CountAsync();
         if (duty > 0) return (duty, "物料职责 (EmpWorkDutyMaster)");