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 物料职责维护(EmpWorkDutyMaster 语义)。员工 + 库位关联。 /// 多租户隔离:所有读写显式限定当前请求租户;员工/库位存在性与补显均限定当前租户,杜绝跨租户绑定;不依赖全局 AOP。 /// [ApiController] [Route("api/s0/warehouse/emp-work-duties")] [NonUnify] public class AdoS0EmpWorkDutiesController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _empRep; private readonly SqlSugarRepository _locRep; private readonly AdoS0ReferenceChecker _refChecker; public AdoS0EmpWorkDutiesController( SqlSugarRepository rep, SqlSugarRepository empRep, SqlSugarRepository locRep, AdoS0ReferenceChecker refChecker) { _rep = rep; _empRep = empRep; _locRep = locRep; _refChecker = refChecker; } [HttpGet] public async Task 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) .WhereIF(!string.IsNullOrWhiteSpace(q.Employee), x => x.Employee.Contains(q.Employee!)) .WhereIF(!string.IsNullOrWhiteSpace(q.Location), x => x.Location == q.Location) .WhereIF(!string.IsNullOrWhiteSpace(q.ItemNum), x => (x.ItemNum != null && x.ItemNum.Contains(q.ItemNum!)) || (x.ItemNum1 != null && x.ItemNum1.Contains(q.ItemNum!)) || (x.ItemNum2 != null && x.ItemNum2.Contains(q.ItemNum!))) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Employee.Contains(q.Keyword!) || (x.ItemNum != null && x.ItemNum.Contains(q.Keyword!)) || (x.ItemNum1 != null && x.ItemNum1.Contains(q.Keyword!)) || (x.ItemNum2 != null && x.ItemNum2.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query .OrderBy(x => x.Employee) .Skip((q.Page - 1) * q.PageSize) .Take(q.PageSize) .ToListAsync(); await ApplyDisplayFieldsAsync(list, tenantId); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public async Task GetAsync(long 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 { item }, tenantId); return Ok(item); } [HttpPost] public async Task 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, Employee = dto.Employee, ItemNum = itemNum, ItemNum1 = itemNum1, ItemNum2 = itemNum2, Location = dto.Location, ProdLine = dto.ProdLine, Duty = dto.Duty, EmpType = dto.EmpType, Ufld2 = dto.Ufld2, CreateUser = dto.CreateUser, CreateTime = DateTime.Now }; await _rep.AsInsertable(entity).ExecuteReturnEntityAsync(); return Ok(entity); } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0EmpWorkDutyUpsertDto dto) { 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; entity.Employee = dto.Employee; entity.ItemNum = itemNum; entity.ItemNum1 = itemNum1; entity.ItemNum2 = itemNum2; entity.Location = dto.Location; entity.ProdLine = dto.ProdLine; entity.Duty = dto.Duty; entity.EmpType = dto.EmpType; entity.Ufld2 = dto.Ufld2; entity.UpdateUser = dto.UpdateUser; entity.UpdateTime = DateTime.Now; await _rep.AsUpdateable(entity).ExecuteCommandAsync(); return Ok(entity); } [HttpDelete("{id:long}")] public async Task DeleteAsync(long 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 = "删除成功" }); } /// 员工与库位必须属于当前租户(同 domain),杜绝为他租户员工分配职责/绑定他租户库位。 private async Task 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 DutyMap = new(StringComparer.OrdinalIgnoreCase) { ["iss-po"] = "采购收货", ["iss-so"] = "销售发货", ["iss-wo"] = "发料", ["packing"] = "退料", ["rct-unp"] = "计划外入库", ["rct-wo"] = "入库", ["Up-Shelf"] = "上架" }; /// 补显员工名 / 库位名。关联 EmployeeMaster、LocationMaster 均限定当前租户,避免跨租户补错。 private async Task ApplyDisplayFieldsAsync(List duties, long tenantId) { if (duties.Count == 0) return; var domainCodes = duties.Select(d => d.DomainCode).Distinct().ToList(); var employees = await _empRep.AsQueryable() .Where(e => e.TenantId == tenantId && domainCodes.Contains(e.DomainCode)) .ToListAsync(); var locations = await _locRep.AsQueryable() .Where(l => l.TenantId == tenantId && domainCodes.Contains(l.DomainCode)) .ToListAsync(); foreach (var duty in duties) { var emp = employees.Find(e => string.Equals(e.DomainCode, duty.DomainCode, StringComparison.OrdinalIgnoreCase) && string.Equals(e.Employee, duty.Employee, StringComparison.OrdinalIgnoreCase)); duty.EmployeeName = emp?.Name; if (!string.IsNullOrWhiteSpace(duty.Location)) { var loc = locations.Find(l => string.Equals(l.DomainCode, duty.DomainCode, StringComparison.OrdinalIgnoreCase) && string.Equals(l.Location, duty.Location, StringComparison.OrdinalIgnoreCase)); duty.LocationName = $"{duty.Location} {loc?.Descr ?? ""}".Trim(); } // Duty 翻译(原始多值逗号/空格拼接场景兼容) if (!string.IsNullOrWhiteSpace(duty.Duty)) { var parts = duty.Duty.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); duty.DutyDisplay = string.Join("、", parts.Select(p => DutyMap.TryGetValue(p.Trim(), out var cn) ? cn : p.Trim())); } duty.ItemNumDisplay = !string.IsNullOrWhiteSpace(duty.ItemNum) ? duty.ItemNum : $"{duty.ItemNum1 ?? ""} ~ {duty.ItemNum2 ?? ""}".Trim(' ', '~'); } } private static string? ValidateMaterialScope( AdoS0EmpWorkDutyUpsertDto dto, out string? itemNum, out string? itemNum1, out string? itemNum2) { itemNum = string.IsNullOrWhiteSpace(dto.ItemNum) ? null : dto.ItemNum.Trim(); itemNum1 = string.IsNullOrWhiteSpace(dto.ItemNum1) ? null : dto.ItemNum1.Trim(); itemNum2 = string.IsNullOrWhiteSpace(dto.ItemNum2) ? null : dto.ItemNum2.Trim(); if (!string.IsNullOrWhiteSpace(itemNum)) { itemNum1 = null; itemNum2 = null; return null; } if (string.IsNullOrWhiteSpace(itemNum1) || string.IsNullOrWhiteSpace(itemNum2)) return "请填写单物料编码,或完整填写物料编码起/止区间"; if (string.Compare(itemNum1, itemNum2, StringComparison.OrdinalIgnoreCase) > 0) return "物料编码起不能大于物料编码止"; return null; } }