using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse; using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Warehouse; /// /// S0 库位主数据(LocationMaster 语义)+ 货架明细(LocationShelfMaster)主从保存 /// [ApiController] [Route("api/s0/warehouse/locations")] [AllowAnonymous] [NonUnify] public class AdoS0LocationsController : ControllerBase { private const int MaxShelves = 5000; private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _shelfRep; private readonly AdoS0ReferenceChecker _refChecker; public AdoS0LocationsController( SqlSugarRepository rep, SqlSugarRepository shelfRep, AdoS0ReferenceChecker refChecker) { _rep = rep; _shelfRep = shelfRep; _refChecker = refChecker; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0LocationQueryDto q) { (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable() .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) .WhereIF(!string.IsNullOrWhiteSpace(q.Typed), x => x.Typed == q.Typed) .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Location.Contains(q.Keyword!) || (x.Descr != null && x.Descr.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query .OrderBy(x => x.Typed == "Supp" ? 2 : 0) .OrderBy(x => x.Location) .Skip((q.Page - 1) * q.PageSize) .Take(q.PageSize) .ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } /// /// 库位详情(含货架明细,供编辑回显)。货架按关联口径 tenant_id + domain_code + location 拉取。 /// [HttpGet("{id:long}")] public async Task GetAsync(long id) { var item = await _rep.GetByIdAsync(id); if (item == null) return NotFound(); var shelves = await LoadShelvesAsync(item); var detail = new AdoS0LocationDetailDto { Id = item.Id, CompanyRefId = item.CompanyRefId, FactoryRefId = item.FactoryRefId, DomainCode = item.DomainCode, Location = item.Location, Descr = item.Descr, Storer = item.Storer, Typed = item.Typed, PhysicalAddress = item.PhysicalAddress, IsActive = item.IsActive, CreateUser = item.CreateUser, CreateTime = item.CreateTime, UpdateUser = item.UpdateUser, UpdateTime = item.UpdateTime, Shelves = shelves }; return Ok(detail); } [HttpGet("options")] public async Task GetOptionsAsync( [FromQuery] long? companyRefId, [FromQuery] long? factoryRefId, [FromQuery] string? domainCode, [FromQuery] string? keyword, [FromQuery] bool? isActive, [FromQuery] int? limit) { var enabledFilter = isActive ?? true; var take = Math.Clamp(limit ?? 200, 1, 500); var list = await _rep.AsQueryable() .WhereIF(companyRefId.HasValue, x => x.CompanyRefId == companyRefId!.Value) .WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value) .WhereIF(!string.IsNullOrWhiteSpace(domainCode), x => x.DomainCode == domainCode) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Location.Contains(keyword!) || (x.Descr != null && x.Descr.Contains(keyword!))) .Where(x => x.IsActive == enabledFilter) .OrderBy(x => x.Location) .Take(take) .Select(x => new S0LocationOptionRow { Value = x.Location, Label = x.Descr == null || x.Descr == "" ? x.Location : x.Location + " / " + x.Descr, Code = x.Location, Name = x.Descr, DomainCode = x.DomainCode, IsActive = x.IsActive }) .ToListAsync(); return Ok(list); } /// /// 新增库位(含货架明细,主从同事务保存)。 /// [HttpPost] public async Task CreateAsync([FromBody] AdoS0LocationUpsertDto dto) { var (shelfError, shelfItems) = ValidateShelves(dto.Shelves); if (shelfError != null) return shelfError; if (await _rep.IsAnyAsync(x => x.FactoryRefId == dto.FactoryRefId && x.Location == dto.Location)) return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在"); var now = DateTime.Now; var entity = new AdoS0LocationMaster { CompanyRefId = dto.CompanyRefId, FactoryRefId = dto.FactoryRefId, DomainCode = dto.DomainCode ?? string.Empty, Location = dto.Location, Descr = dto.Descr, Storer = dto.Storer, Typed = dto.Typed, PhysicalAddress = dto.PhysicalAddress, IsActive = dto.IsActive, CreateUser = dto.CreateUser, CreateTime = now }; var db = _rep.Context; try { await db.Ado.BeginTranAsync(); var saved = await _rep.AsInsertable(entity).ExecuteReturnEntityAsync(); if (shelfItems.Count > 0) { var shelfEntities = BuildShelfEntities(saved, shelfItems, dto.CreateUser, now); await _shelfRep.AsInsertable(shelfEntities).ExecuteCommandAsync(); } await db.Ado.CommitTranAsync(); return Ok(saved); } catch (Exception ex) { await db.Ado.RollbackTranAsync(); return MapWriteException(ex); } } /// /// 编辑库位(含货架明细 FULL Replace,主从同事务保存)。库位编码不可修改。 /// [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0LocationUpsertDto dto) { var (shelfError, shelfItems) = ValidateShelves(dto.Shelves); if (shelfError != null) return shelfError; var entity = await _rep.GetByIdAsync(id); 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)) return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "库位编码已存在"); var now = DateTime.Now; entity.CompanyRefId = dto.CompanyRefId; entity.FactoryRefId = dto.FactoryRefId; entity.DomainCode = dto.DomainCode ?? string.Empty; // entity.Location 保持不变(不可修改) entity.Descr = dto.Descr; entity.Storer = dto.Storer; entity.Typed = dto.Typed; entity.PhysicalAddress = dto.PhysicalAddress; entity.IsActive = dto.IsActive; entity.UpdateUser = dto.UpdateUser; entity.UpdateTime = now; var db = _rep.Context; try { await db.Ado.BeginTranAsync(); await _rep.AsUpdateable(entity).ExecuteCommandAsync(); // FULL Replace:删除本库位当前作用域(tenant + domain + location)下全部货架,再整体重插 await _shelfRep.AsDeleteable() .Where(x => x.DomainCode == entity.DomainCode && x.Location == entity.Location) .ExecuteCommandAsync(); if (shelfItems.Count > 0) { var shelfEntities = BuildShelfEntities(entity, shelfItems, dto.UpdateUser, now); await _shelfRep.AsInsertable(shelfEntities).ExecuteCommandAsync(); } await db.Ado.CommitTranAsync(); return Ok(entity); } catch (Exception ex) { await db.Ado.RollbackTranAsync(); return MapWriteException(ex); } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { var item = await _rep.GetByIdAsync(id); if (item == null) return NotFound(); // 保持既有删除契约:存在货架(或其它引用)时拦截,不做级联删除 var refInfo = await _refChecker.LocationReferencesAsync(item.Location); if (refInfo is { } r) return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DeleteBlocked, $"存在 {r.Count} 条 {r.Table} 引用该库位,无法删除"); await _rep.DeleteAsync(item); return Ok(new { message = "删除成功" }); } // ==================== 私有:货架明细主从辅助 ==================== /// /// 按关联口径(tenant 自动过滤 + domain_code + location)拉取库位下货架明细。 /// private async Task> LoadShelvesAsync(AdoS0LocationMaster master) { return await _shelfRep.AsQueryable() .Where(x => x.DomainCode == master.DomainCode && x.Location == master.Location) .OrderBy(x => x.InvShelf) .Select(x => new AdoS0LocationShelfInputDto { Id = x.Id, InvShelf = x.InvShelf, Descr = x.Descr, Area = x.Area }) .ToListAsync(); } /// /// 服务端货架明细校验 + 规范化(Trim、长度、请求内去重、数量上限)。前端校验只是体验,服务端为准。 /// private static (IActionResult? Error, List Items) ValidateShelves(List? shelves) { var items = shelves ?? new List(); if (items.Count > MaxShelves) return (AdoS0ApiErrors.InvalidRequest($"货架明细数量 {items.Count} 超过上限 {MaxShelves},请缩小货架序号范围/层数/列数后重试"), new()); var normalized = new List(items.Count); // 去重按数据库不区分大小写口径(MySQL 默认 ci 排序规则),避免与唯一索引冲突 var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var s in items) { var code = (s.InvShelf ?? string.Empty).Trim(); if (code.Length == 0) return (AdoS0ApiErrors.InvalidRequest("存在货架编码为空的明细行,请填写货架编码或删除该行"), new()); if (code.Length > 100) return (AdoS0ApiErrors.InvalidRequest($"货架编码 '{code}' 超过 100 字符上限"), new()); var descr = s.Descr?.Trim(); if (descr is { Length: > 255 }) return (AdoS0ApiErrors.InvalidRequest($"货架 '{code}' 的描述超过 255 字符上限"), new()); var area = s.Area?.Trim(); if (area is { Length: > 100 }) return (AdoS0ApiErrors.InvalidRequest($"货架 '{code}' 的区域超过 100 字符上限"), new()); if (!seen.Add(code)) return (AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateDetailItem, $"货架编码重复:{code}"), new()); normalized.Add(new AdoS0LocationShelfInputDto { InvShelf = code, Descr = string.IsNullOrEmpty(descr) ? null : descr, Area = string.IsNullOrEmpty(area) ? null : area }); } return (null, normalized); } /// /// 由库位主表统一赋值货架作用域字段(tenant 由 ITenantIdFilter 自动注入,此处不设)。 /// private static List BuildShelfEntities( AdoS0LocationMaster master, List items, string? actingUser, DateTime now) { return items.Select(s => new AdoS0LocationShelfMaster { CompanyRefId = master.CompanyRefId, FactoryRefId = master.FactoryRefId, DomainCode = master.DomainCode, Location = master.Location, InvShelf = s.InvShelf, Descr = s.Descr, Area = s.Area, CreateUser = actingUser, CreateTime = now }).ToList(); } /// /// 写入异常映射:唯一键冲突 → 清晰业务错误;其余 → 500(事务已回滚)。 /// private static IActionResult MapWriteException(Exception ex) { var msg = ex.Message + " " + (ex.InnerException?.Message ?? string.Empty); if (msg.Contains("Duplicate entry", StringComparison.OrdinalIgnoreCase) || msg.Contains("1062")) return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateDetailItem, "货架编码在同一库位内重复(唯一约束冲突),保存已回滚"); return AdoS0ApiErrors.InternalServerError("库位与货架明细保存失败,已整体回滚"); } }