|
|
@@ -5,11 +5,11 @@ using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse;
|
|
|
|
|
|
/// <summary>
|
|
|
-/// S0 库位主数据(LocationMaster 语义)+ 货架明细(LocationShelfMaster)主从保存
|
|
|
+/// S0 库位主数据(LocationMaster 语义)+ 货架明细(LocationShelfMaster)主从保存。
|
|
|
+/// 多租户隔离:所有读写显式限定当前请求租户(AdoS0TenantScope.TryResolveRequired),不依赖全局 AOP。
|
|
|
/// </summary>
|
|
|
[ApiController]
|
|
|
[Route("api/s0/warehouse/locations")]
|
|
|
-[AllowAnonymous]
|
|
|
[NonUnify]
|
|
|
public class AdoS0LocationsController : ControllerBase
|
|
|
{
|
|
|
@@ -32,9 +32,11 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
[HttpGet]
|
|
|
public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0LocationQueryDto 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,15 +57,17 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 库位详情(含货架明细,供编辑回显)。货架按关联口径 tenant_id + domain_code + location 拉取。
|
|
|
+ /// 库位详情(含货架明细,供编辑回显)。按 id + 当前租户定位;他租户记录统一返回 NotFound(不泄露存在性)。
|
|
|
/// </summary>
|
|
|
[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();
|
|
|
|
|
|
- var shelves = await LoadShelvesAsync(item);
|
|
|
+ var shelves = await LoadShelvesAsync(item, tenantId);
|
|
|
var detail = new AdoS0LocationDetailDto
|
|
|
{
|
|
|
Id = item.Id,
|
|
|
@@ -94,10 +98,12 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
[FromQuery] bool? isActive,
|
|
|
[FromQuery] int? limit)
|
|
|
{
|
|
|
+ if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
|
|
|
var enabledFilter = isActive ?? true;
|
|
|
var take = Math.Clamp(limit ?? 200, 1, 500);
|
|
|
|
|
|
var list = await _rep.AsQueryable()
|
|
|
+ .Where(x => x.TenantId == tenantId)
|
|
|
.WhereIF(companyRefId.HasValue, x => x.CompanyRefId == companyRefId!.Value)
|
|
|
.WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(domainCode), x => x.DomainCode == domainCode)
|
|
|
@@ -121,20 +127,23 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 新增库位(含货架明细,主从同事务保存)。
|
|
|
+ /// 新增库位(含货架明细,主从同事务保存)。tenant_id 由服务端按当前租户赋值。
|
|
|
/// </summary>
|
|
|
[HttpPost]
|
|
|
public async Task<IActionResult> CreateAsync([FromBody] AdoS0LocationUpsertDto dto)
|
|
|
{
|
|
|
+ if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
|
|
|
+
|
|
|
var (shelfError, shelfItems) = ValidateShelves(dto.Shelves);
|
|
|
if (shelfError != null) return shelfError;
|
|
|
|
|
|
- if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
|
|
|
+ if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
|
|
|
return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在");
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
var entity = new AdoS0LocationMaster
|
|
|
{
|
|
|
+ TenantId = tenantId,
|
|
|
CompanyRefId = dto.CompanyRefId,
|
|
|
FactoryRefId = dto.FactoryRefId,
|
|
|
DomainCode = dto.DomainCode ?? string.Empty,
|
|
|
@@ -157,7 +166,8 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
|
|
|
if (shelfItems.Count > 0)
|
|
|
{
|
|
|
- var shelfEntities = BuildShelfEntities(saved, shelfItems, dto.CreateUser, now);
|
|
|
+ var shelfEntities = BuildShelfEntities(saved, shelfItems, tenantId, dto.CreateUser, now);
|
|
|
+ AssertTenant(tenantId, saved, shelfEntities);
|
|
|
await _shelfRep.AsInsertable(shelfEntities).ExecuteCommandAsync();
|
|
|
}
|
|
|
|
|
|
@@ -172,25 +182,28 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 编辑库位(含货架明细 FULL Replace,主从同事务保存)。库位编码不可修改。
|
|
|
+ /// 编辑库位(含货架明细 FULL Replace,主从同事务保存)。按 id + 当前租户定位;库位编码不可修改;忽略前端传入的 tenant。
|
|
|
/// </summary>
|
|
|
[HttpPut("{id:long}")]
|
|
|
public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0LocationUpsertDto dto)
|
|
|
{
|
|
|
+ if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
|
|
|
+
|
|
|
var (shelfError, shelfItems) = ValidateShelves(dto.Shelves);
|
|
|
if (shelfError != null) return shelfError;
|
|
|
|
|
|
- var entity = await _rep.GetByIdAsync(id);
|
|
|
+ var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
|
|
|
if (entity == null) return NotFound();
|
|
|
|
|
|
// 库位编码不可修改:以库存原值为准,前端传值须一致
|
|
|
if (!string.Equals(entity.Location, dto.Location?.Trim(), StringComparison.Ordinal))
|
|
|
return AdoS0ApiErrors.InvalidRequest("库位编码不可修改");
|
|
|
|
|
|
- if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
|
|
|
+ if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location))
|
|
|
return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在");
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
+ // entity.TenantId 保持不变(== tenantId);不接受前端修改 tenant
|
|
|
entity.CompanyRefId = dto.CompanyRefId;
|
|
|
entity.FactoryRefId = dto.FactoryRefId;
|
|
|
entity.DomainCode = dto.DomainCode ?? string.Empty;
|
|
|
@@ -210,14 +223,15 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
|
|
|
await _rep.AsUpdateable(entity).ExecuteCommandAsync();
|
|
|
|
|
|
- // FULL Replace:删除本库位当前作用域(tenant + domain + location)下全部货架,再整体重插
|
|
|
+ // FULL Replace:显式限定 tenant + domain + location 删本库位全部货架,再整体重插(禁止仅按 domain+location)
|
|
|
await _shelfRep.AsDeleteable()
|
|
|
- .Where(x => x.DomainCode == entity.DomainCode && x.Location == entity.Location)
|
|
|
+ .Where(x => x.TenantId == tenantId && x.DomainCode == entity.DomainCode && x.Location == entity.Location)
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
if (shelfItems.Count > 0)
|
|
|
{
|
|
|
- var shelfEntities = BuildShelfEntities(entity, shelfItems, dto.UpdateUser, now);
|
|
|
+ var shelfEntities = BuildShelfEntities(entity, shelfItems, tenantId, dto.UpdateUser, now);
|
|
|
+ AssertTenant(tenantId, entity, shelfEntities);
|
|
|
await _shelfRep.AsInsertable(shelfEntities).ExecuteCommandAsync();
|
|
|
}
|
|
|
|
|
|
@@ -234,11 +248,13 @@ public class AdoS0LocationsController : 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.LocationReferencesAsync(item.Location);
|
|
|
+ // 引用检查限定 当前租户 + domain + location:不因他租户同编码货架而阻止/误删
|
|
|
+ var refInfo = await _refChecker.LocationReferencesAsync(tenantId, item.DomainCode, item.Location);
|
|
|
if (refInfo is { } r)
|
|
|
return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DeleteBlocked,
|
|
|
$"存在 {r.Count} 条 {r.Table} 引用该库位,无法删除");
|
|
|
@@ -250,12 +266,12 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
// ==================== 私有:货架明细主从辅助 ====================
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 按关联口径(tenant 自动过滤 + domain_code + location)拉取库位下货架明细。
|
|
|
+ /// 按关联口径(当前租户 + domain_code + location)拉取库位下货架明细。
|
|
|
/// </summary>
|
|
|
- private async Task<List<AdoS0LocationShelfInputDto>> LoadShelvesAsync(AdoS0LocationMaster master)
|
|
|
+ private async Task<List<AdoS0LocationShelfInputDto>> LoadShelvesAsync(AdoS0LocationMaster master, long tenantId)
|
|
|
{
|
|
|
return await _shelfRep.AsQueryable()
|
|
|
- .Where(x => x.DomainCode == master.DomainCode && x.Location == master.Location)
|
|
|
+ .Where(x => x.TenantId == tenantId && x.DomainCode == master.DomainCode && x.Location == master.Location)
|
|
|
.OrderBy(x => x.InvShelf)
|
|
|
.Select(x => new AdoS0LocationShelfInputDto
|
|
|
{
|
|
|
@@ -309,13 +325,14 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 由库位主表统一赋值货架作用域字段(tenant 由 ITenantIdFilter 自动注入,此处不设)。
|
|
|
+ /// 由库位主表 + 当前租户统一赋值货架作用域字段(tenant_id 显式赋值,不依赖 AOP)。
|
|
|
/// </summary>
|
|
|
private static List<AdoS0LocationShelfMaster> BuildShelfEntities(
|
|
|
- AdoS0LocationMaster master, List<AdoS0LocationShelfInputDto> items, string? actingUser, DateTime now)
|
|
|
+ AdoS0LocationMaster master, List<AdoS0LocationShelfInputDto> items, long tenantId, string? actingUser, DateTime now)
|
|
|
{
|
|
|
return items.Select(s => new AdoS0LocationShelfMaster
|
|
|
{
|
|
|
+ TenantId = tenantId,
|
|
|
CompanyRefId = master.CompanyRefId,
|
|
|
FactoryRefId = master.FactoryRefId,
|
|
|
DomainCode = master.DomainCode,
|
|
|
@@ -328,6 +345,15 @@ public class AdoS0LocationsController : ControllerBase
|
|
|
}).ToList();
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 落库前断言:主表与全部货架的 TenantId 均等于当前租户且非空非 0(W-01 NULL 防线)。
|
|
|
+ /// </summary>
|
|
|
+ private static void AssertTenant(long tenantId, AdoS0LocationMaster master, List<AdoS0LocationShelfMaster> shelves)
|
|
|
+ {
|
|
|
+ if (tenantId <= 0 || master.TenantId != tenantId || shelves.Any(s => s.TenantId != tenantId))
|
|
|
+ throw new InvalidOperationException("租户归属断言失败:主表/货架 TenantId 必须等于当前租户且非空。");
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 写入异常映射:唯一键冲突 → 清晰业务错误;其余 → 500(事务已回滚)。
|
|
|
/// </summary>
|